From 849682c286bdb0539f99102c91a793704636751f Mon Sep 17 00:00:00 2001 From: Francesco Ballarin Date: Tue, 3 Mar 2026 15:04:38 +0000 Subject: [PATCH] New upstream version 9.5.2+dfsg4 --- Web/Core/CMakeLists.txt | 11 + Web/Core/Testing/CMakeLists.txt | 9 + Web/Core/Testing/Cxx/CMakeLists.txt | 5 + Web/Core/Testing/Cxx/TestDataEncoder.cxx | 117 ++ .../Data/Baseline/TestDataEncoder.png.sha512 | 1 + .../Baseline/TestDataEncoder_1.png.sha512 | 1 + .../TestRemoteInteractionAdapter.png.sha512 | 1 + Web/Core/Testing/Python/CMakeLists.txt | 10 + Web/Core/Testing/Python/TestDataEncoder.py | 87 + Web/Core/Testing/Python/TestObjectIdMap.py | 40 + .../Python/TestRemoteInteractionAdapter.py | 99 ++ .../Python/TestWebApplicationMemory.py | 42 + Web/Core/vtk.module | 31 + Web/Core/vtkDataEncoder.cxx | 334 ++++ Web/Core/vtkDataEncoder.h | 110 ++ Web/Core/vtkObjectIdMap.cxx | 123 ++ Web/Core/vtkObjectIdMap.h | 74 + Web/Core/vtkRemoteInteractionAdapter.cxx | 291 ++++ Web/Core/vtkRemoteInteractionAdapter.h | 85 + Web/Core/vtkWebApplication.cxx | 464 ++++++ Web/Core/vtkWebApplication.h | 144 ++ Web/Core/vtkWebInteractionEvent.cxx | 36 + Web/Core/vtkWebInteractionEvent.h | 96 ++ Web/Core/vtkWebUtilities.cxx | 118 ++ Web/Core/vtkWebUtilities.h | 52 + Web/Python/CMakeLists.txt | 25 + Web/Python/Testing/CMakeLists.txt | 3 + Web/Python/Testing/Python/CMakeLists.txt | 4 + .../Python/TestSerializeRenderWindow.py | 43 + Web/Python/vtk.module | 22 + Web/Python/vtkmodules/web/__init__.py | 62 + Web/Python/vtkmodules/web/camera.py | 640 ++++++++ Web/Python/vtkmodules/web/dataset_builder.py | 620 ++++++++ Web/Python/vtkmodules/web/errors.py | 12 + Web/Python/vtkmodules/web/protocols.py | 842 ++++++++++ Web/Python/vtkmodules/web/query_data_model.py | 182 +++ .../web/render_window_serializer.py | 1410 +++++++++++++++++ Web/Python/vtkmodules/web/testing.py | 788 +++++++++ Web/Python/vtkmodules/web/utils.py | 211 +++ Web/Python/vtkmodules/web/venv.py | 36 + Web/Python/vtkmodules/web/vtkjs_helper.py | 283 ++++ Web/Python/vtkmodules/web/wslink.py | 67 + Web/WebAssembly/CMakeLists.txt | 128 ++ Web/WebAssembly/Testing/CMakeLists.txt | 8 + .../Testing/JavaScript/CMakeLists.txt | 22 + .../JavaScript/testBindRenderWindow.mjs | 53 + .../Testing/JavaScript/testBlobs.mjs | 52 + .../Testing/JavaScript/testInitialize.mjs | 27 + .../Testing/JavaScript/testInvoke.mjs | 49 + .../testOSMesaRenderWindowPatch.mjs | 45 + .../Testing/JavaScript/testSkipProperty.mjs | 59 + .../Testing/JavaScript/testStates.mjs | 60 + Web/WebAssembly/post.js | 1 + Web/WebAssembly/vtk.module | 17 + Web/WebAssembly/vtkWasmSceneManager.cxx | 232 +++ Web/WebAssembly/vtkWasmSceneManager.h | 114 ++ .../vtkWasmSceneManagerEmBinding.cxx | 463 ++++++ Web/WebGLExporter/CMakeLists.txt | 42 + Web/WebGLExporter/glMatrix.js | 32 + Web/WebGLExporter/vtk.module | 23 + Web/WebGLExporter/vtkPVWebGLExporter.cxx | 125 ++ Web/WebGLExporter/vtkPVWebGLExporter.h | 36 + Web/WebGLExporter/vtkWebGLDataSet.cxx | 235 +++ Web/WebGLExporter/vtkWebGLDataSet.h | 68 + Web/WebGLExporter/vtkWebGLExporter.cxx | 788 +++++++++ Web/WebGLExporter/vtkWebGLExporter.h | 101 ++ Web/WebGLExporter/vtkWebGLObject.cxx | 201 +++ Web/WebGLExporter/vtkWebGLObject.h | 92 ++ Web/WebGLExporter/vtkWebGLPolyData.cxx | 783 +++++++++ Web/WebGLExporter/vtkWebGLPolyData.h | 66 + Web/WebGLExporter/vtkWebGLWidget.cxx | 157 ++ Web/WebGLExporter/vtkWebGLWidget.h | 54 + Web/WebGLExporter/webglRenderer.js | 1307 +++++++++++++++ 73 files changed, 13071 insertions(+) create mode 100644 Web/Core/CMakeLists.txt create mode 100644 Web/Core/Testing/CMakeLists.txt create mode 100644 Web/Core/Testing/Cxx/CMakeLists.txt create mode 100644 Web/Core/Testing/Cxx/TestDataEncoder.cxx create mode 100644 Web/Core/Testing/Data/Baseline/TestDataEncoder.png.sha512 create mode 100644 Web/Core/Testing/Data/Baseline/TestDataEncoder_1.png.sha512 create mode 100644 Web/Core/Testing/Data/Baseline/TestRemoteInteractionAdapter.png.sha512 create mode 100644 Web/Core/Testing/Python/CMakeLists.txt create mode 100644 Web/Core/Testing/Python/TestDataEncoder.py create mode 100644 Web/Core/Testing/Python/TestObjectIdMap.py create mode 100644 Web/Core/Testing/Python/TestRemoteInteractionAdapter.py create mode 100644 Web/Core/Testing/Python/TestWebApplicationMemory.py create mode 100644 Web/Core/vtk.module create mode 100644 Web/Core/vtkDataEncoder.cxx create mode 100644 Web/Core/vtkDataEncoder.h create mode 100644 Web/Core/vtkObjectIdMap.cxx create mode 100644 Web/Core/vtkObjectIdMap.h create mode 100644 Web/Core/vtkRemoteInteractionAdapter.cxx create mode 100644 Web/Core/vtkRemoteInteractionAdapter.h create mode 100644 Web/Core/vtkWebApplication.cxx create mode 100644 Web/Core/vtkWebApplication.h create mode 100644 Web/Core/vtkWebInteractionEvent.cxx create mode 100644 Web/Core/vtkWebInteractionEvent.h create mode 100644 Web/Core/vtkWebUtilities.cxx create mode 100644 Web/Core/vtkWebUtilities.h create mode 100644 Web/Python/CMakeLists.txt create mode 100644 Web/Python/Testing/CMakeLists.txt create mode 100644 Web/Python/Testing/Python/CMakeLists.txt create mode 100644 Web/Python/Testing/Python/TestSerializeRenderWindow.py create mode 100644 Web/Python/vtk.module create mode 100644 Web/Python/vtkmodules/web/__init__.py create mode 100644 Web/Python/vtkmodules/web/camera.py create mode 100644 Web/Python/vtkmodules/web/dataset_builder.py create mode 100644 Web/Python/vtkmodules/web/errors.py create mode 100644 Web/Python/vtkmodules/web/protocols.py create mode 100644 Web/Python/vtkmodules/web/query_data_model.py create mode 100644 Web/Python/vtkmodules/web/render_window_serializer.py create mode 100644 Web/Python/vtkmodules/web/testing.py create mode 100644 Web/Python/vtkmodules/web/utils.py create mode 100644 Web/Python/vtkmodules/web/venv.py create mode 100644 Web/Python/vtkmodules/web/vtkjs_helper.py create mode 100644 Web/Python/vtkmodules/web/wslink.py create mode 100644 Web/WebAssembly/CMakeLists.txt create mode 100644 Web/WebAssembly/Testing/CMakeLists.txt create mode 100644 Web/WebAssembly/Testing/JavaScript/CMakeLists.txt create mode 100644 Web/WebAssembly/Testing/JavaScript/testBindRenderWindow.mjs create mode 100644 Web/WebAssembly/Testing/JavaScript/testBlobs.mjs create mode 100644 Web/WebAssembly/Testing/JavaScript/testInitialize.mjs create mode 100644 Web/WebAssembly/Testing/JavaScript/testInvoke.mjs create mode 100644 Web/WebAssembly/Testing/JavaScript/testOSMesaRenderWindowPatch.mjs create mode 100644 Web/WebAssembly/Testing/JavaScript/testSkipProperty.mjs create mode 100644 Web/WebAssembly/Testing/JavaScript/testStates.mjs create mode 100644 Web/WebAssembly/post.js create mode 100644 Web/WebAssembly/vtk.module create mode 100644 Web/WebAssembly/vtkWasmSceneManager.cxx create mode 100644 Web/WebAssembly/vtkWasmSceneManager.h create mode 100644 Web/WebAssembly/vtkWasmSceneManagerEmBinding.cxx create mode 100644 Web/WebGLExporter/CMakeLists.txt create mode 100644 Web/WebGLExporter/glMatrix.js create mode 100644 Web/WebGLExporter/vtk.module create mode 100644 Web/WebGLExporter/vtkPVWebGLExporter.cxx create mode 100644 Web/WebGLExporter/vtkPVWebGLExporter.h create mode 100644 Web/WebGLExporter/vtkWebGLDataSet.cxx create mode 100644 Web/WebGLExporter/vtkWebGLDataSet.h create mode 100644 Web/WebGLExporter/vtkWebGLExporter.cxx create mode 100644 Web/WebGLExporter/vtkWebGLExporter.h create mode 100644 Web/WebGLExporter/vtkWebGLObject.cxx create mode 100644 Web/WebGLExporter/vtkWebGLObject.h create mode 100644 Web/WebGLExporter/vtkWebGLPolyData.cxx create mode 100644 Web/WebGLExporter/vtkWebGLPolyData.h create mode 100644 Web/WebGLExporter/vtkWebGLWidget.cxx create mode 100644 Web/WebGLExporter/vtkWebGLWidget.h create mode 100644 Web/WebGLExporter/webglRenderer.js diff --git a/Web/Core/CMakeLists.txt b/Web/Core/CMakeLists.txt new file mode 100644 index 000000000..5ce21cbc9 --- /dev/null +++ b/Web/Core/CMakeLists.txt @@ -0,0 +1,11 @@ +set(classes + vtkDataEncoder + vtkObjectIdMap + vtkRemoteInteractionAdapter + vtkWebApplication + vtkWebInteractionEvent + vtkWebUtilities) + +vtk_module_add_module(VTK::WebCore + CLASSES ${classes}) +vtk_add_test_mangling(VTK::WebCore) diff --git a/Web/Core/Testing/CMakeLists.txt b/Web/Core/Testing/CMakeLists.txt new file mode 100644 index 000000000..c4378c861 --- /dev/null +++ b/Web/Core/Testing/CMakeLists.txt @@ -0,0 +1,9 @@ +if (NOT vtk_testing_cxx_disabled) + add_subdirectory(Cxx) +endif () + +if (VTK_WRAP_PYTHON) + vtk_module_test_data( + Data/remote_events.json) + add_subdirectory(Python) +endif () diff --git a/Web/Core/Testing/Cxx/CMakeLists.txt b/Web/Core/Testing/Cxx/CMakeLists.txt new file mode 100644 index 000000000..1e60e4527 --- /dev/null +++ b/Web/Core/Testing/Cxx/CMakeLists.txt @@ -0,0 +1,5 @@ +vtk_add_test_cxx(vtkWebCoreCxxTests tests + NO_VALID + TestDataEncoder.cxx) + +vtk_test_cxx_executable(vtkWebCoreCxxTests tests) diff --git a/Web/Core/Testing/Cxx/TestDataEncoder.cxx b/Web/Core/Testing/Cxx/TestDataEncoder.cxx new file mode 100644 index 000000000..910832319 --- /dev/null +++ b/Web/Core/Testing/Cxx/TestDataEncoder.cxx @@ -0,0 +1,117 @@ +// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen +// SPDX-License-Identifier: BSD-3-Clause +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +vtkSmartPointer GetData() +{ + vtkNew source; + source->SetWholeExtent(0, 256, 0, 256, 0, 0); + + vtkNew caster; + caster->SetInputConnection(source->GetOutputPort()); + caster->SetOutputScalarTypeToUnsignedChar(); + caster->Update(); + return caster->GetOutput(); +} + +bool TestCreate() +{ + vtkLogScopeFunction(INFO); + //-------------------------------------------------------------- + // Create a bunch of instances and ensure it doesn't cause issues + // #18344 + for (int cc = 0; cc < 100; cc++) + { + vtkNew encoder; + } + + std::vector> encoders; + encoders.reserve(100); + for (int cc = 0; cc < 100; cc++) + { + encoders.push_back(vtk::TakeSmartPointer(vtkDataEncoder::New())); + } + return true; +} + +bool TestFlush() +{ + vtkLogScopeFunction(INFO); + constexpr int KEY = 1020; + + vtkNew encoder; + encoder->SetMaxThreads(5); + encoder->Initialize(); + + // call flush without pushing any data. + encoder->Flush(KEY); + + // push some data and then call flush. + for (int cc = 0; cc < 10; cc++) + { + encoder->Push(KEY, GetData(), 50); + } + + encoder->Flush(KEY); + + // call flush again. + encoder->Flush(KEY); + + // push some data and then call flush. + for (int cc = 0; cc < 10; cc++) + { + encoder->Push(KEY, GetData(), 50); + } + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + encoder->Flush(KEY); + + return true; +} + +bool TestLatestOutput() +{ + vtkLogScopeFunction(INFO); + constexpr int KEY = 1020; + + vtkNew encoder; + + vtkSmartPointer result; + if (encoder->GetLatestOutput(KEY, result)) + { + vtkLogF(ERROR, "no output expected!"); + return false; + } + + // push some data and then call flush. + for (int cc = 0; cc < 10; cc++) + { + encoder->Push(KEY, GetData(), 50); + } + + encoder->Flush(KEY); + if (!encoder->GetLatestOutput(KEY, result)) + { + vtkLogF(ERROR, "latest output expected!"); + return false; + } + + return true; +} + +int TestDataEncoder(int /*argc*/, char* /*argv*/[]) +{ + TestCreate(); + TestFlush(); + TestLatestOutput(); + return EXIT_SUCCESS; +} diff --git a/Web/Core/Testing/Data/Baseline/TestDataEncoder.png.sha512 b/Web/Core/Testing/Data/Baseline/TestDataEncoder.png.sha512 new file mode 100644 index 000000000..7e6932c13 --- /dev/null +++ b/Web/Core/Testing/Data/Baseline/TestDataEncoder.png.sha512 @@ -0,0 +1 @@ +19d5717b868631051688354258802885866eaf6fe3bbbdcd2e8410c2aa1b65ff4585d6943c751b54aa54d6b9c8fe50d97bcbf0c061c5fa63959a3d7e653abe0e diff --git a/Web/Core/Testing/Data/Baseline/TestDataEncoder_1.png.sha512 b/Web/Core/Testing/Data/Baseline/TestDataEncoder_1.png.sha512 new file mode 100644 index 000000000..dfe6b6d01 --- /dev/null +++ b/Web/Core/Testing/Data/Baseline/TestDataEncoder_1.png.sha512 @@ -0,0 +1 @@ +9ef24c779e9c2176cfa88c195e3a8f1102bb3ad03593594e84c5c12f785100ac037e180ad7e56726874433bf2b72fe8e72569222de252ae45f4c224c57fd1fe3 diff --git a/Web/Core/Testing/Data/Baseline/TestRemoteInteractionAdapter.png.sha512 b/Web/Core/Testing/Data/Baseline/TestRemoteInteractionAdapter.png.sha512 new file mode 100644 index 000000000..002bcfbc4 --- /dev/null +++ b/Web/Core/Testing/Data/Baseline/TestRemoteInteractionAdapter.png.sha512 @@ -0,0 +1 @@ +b05dd96550cd5a74ed49277cdf5efd635ef5a53b34152c4af90b0f4d007b3d2c46c3a450cf759cea36c2b84efdd3946853198b423bde8ac9ace8deaff446e258 diff --git a/Web/Core/Testing/Python/CMakeLists.txt b/Web/Core/Testing/Python/CMakeLists.txt new file mode 100644 index 000000000..7a62f300b --- /dev/null +++ b/Web/Core/Testing/Python/CMakeLists.txt @@ -0,0 +1,10 @@ +vtk_add_test_python( + TestDataEncoder.py + TestRemoteInteractionAdapter.py + ) + +vtk_add_test_python( + NO_DATA NO_VALID NO_OUTPUT + TestObjectIdMap.py + TestWebApplicationMemory.py + ) diff --git a/Web/Core/Testing/Python/TestDataEncoder.py b/Web/Core/Testing/Python/TestDataEncoder.py new file mode 100644 index 000000000..778a794c1 --- /dev/null +++ b/Web/Core/Testing/Python/TestDataEncoder.py @@ -0,0 +1,87 @@ +import sys +from vtkmodules.vtkFiltersSources import vtkCylinderSource +from vtkmodules.vtkIOCore import vtkBase64Utilities +from vtkmodules.vtkRenderingCore import ( + vtkActor, + vtkPolyDataMapper, + vtkRenderWindow, + vtkRenderer, + vtkWindowToImageFilter, +) +from vtkmodules.vtkTestingRendering import vtkTesting +from vtkmodules.vtkWebCore import vtkDataEncoder +import vtkmodules.vtkRenderingFreeType +import vtkmodules.vtkRenderingOpenGL2 +import array +from vtkmodules.test import Testing + + +class TestDataEncoder(Testing.vtkTest): + def testEncodings(self): + # Render something + cylinder = vtkCylinderSource() + cylinder.SetResolution(8) + + cylinderMapper = vtkPolyDataMapper() + cylinderMapper.SetInputConnection(cylinder.GetOutputPort()) + + cylinderActor = vtkActor() + cylinderActor.SetMapper(cylinderMapper) + cylinderActor.RotateX(30.0) + cylinderActor.RotateY(-45.0) + + ren = vtkRenderer() + renWin = vtkRenderWindow() + renWin.AddRenderer(ren) + ren.AddActor(cylinderActor) + renWin.SetSize(200, 200) + + ren.ResetCamera() + ren.GetActiveCamera().Zoom(1.5) + renWin.Render() + + # Get a vtkImageData with the rendered output + w2if = vtkWindowToImageFilter() + w2if.SetInput(renWin) + w2if.SetShouldRerender(1) + w2if.SetReadFrontBuffer(0) + w2if.Update() + imgData = w2if.GetOutput() + + # Use vtkDataEncoder to convert the image to PNG format and Base64 encode it + encoder = vtkDataEncoder() + base64String = encoder.EncodeAsBase64Png(imgData).encode('ascii') + + # Now Base64 decode the string back to PNG image data bytes + inputArray = array.array('B', base64String) + outputBuffer = bytearray(len(inputArray)) + + try: + utils = vtkBase64Utilities() + except: + print('Unable to import required vtkBase64Utilities') + raise Exception("TestDataEncoder failed.") + + actualLength = utils.DecodeSafely(inputArray, len(inputArray), outputBuffer, len(outputBuffer)) + outputArray = bytearray(actualLength) + outputArray[:] = outputBuffer[0:actualLength] + + # And write those bytes to the disk as an actual PNG image file + with open('TestDataEncoder.png', 'wb') as fd: + fd.write(outputArray) + + # Create a vtkTesting object and specify a baseline image + rtTester = vtkTesting() + for arg in sys.argv[1:]: + rtTester.AddArgument(arg) + rtTester.AddArgument("-V") + rtTester.AddArgument("TestDataEncoder.png") + + # Perform the image comparison test and print out the result. + result = rtTester.RegressionTest("TestDataEncoder.png", 0.05) + + if result == 0: + raise Exception("TestDataEncoder failed.") + +if __name__ == "__main__": + Testing.main([(TestDataEncoder, 'test')]) diff --git a/Web/Core/Testing/Python/TestObjectIdMap.py b/Web/Core/Testing/Python/TestObjectIdMap.py new file mode 100644 index 000000000..6c3d0aeff --- /dev/null +++ b/Web/Core/Testing/Python/TestObjectIdMap.py @@ -0,0 +1,40 @@ +from vtkmodules.vtkCommonCore import vtkObject +from vtkmodules.vtkWebCore import vtkObjectIdMap +from vtkmodules.test import Testing +from vtkmodules.vtkWebCore import vtkWebApplication + +class TestObjectId(Testing.vtkTest): + def testObjId(self): + map = vtkObjectIdMap() + # Just make sure if we call it twice with None, the results match + objId1 = map.GetGlobalId(None) + objId1b = map.GetGlobalId(None) + print('Object ids for None: objId1 => ',objId1,', objId1b => ',objId1b) + self.assertTrue(objId1 == objId1b) + + object2 = vtkObject() + addr2 = object2.__this__ + addr2 = addr2[1:addr2.find('_', 1)] + addr2 = int(addr2, 16) + + object3 = vtkObject() + addr3 = object3.__this__ + addr3 = addr3[1:addr3.find('_', 1)] + addr3 = int(addr3, 16) + + # insert the bigger address first + if (addr2 < addr3): + object2, object3 = object3, object2 + + objId2 = map.GetGlobalId(object2) + objId2b = map.GetGlobalId(object2) + print('Object ids for object2: objId2 => ',objId2,', objId2b => ',objId2b) + self.assertTrue(objId2 == objId2b) + + objId3 = map.GetGlobalId(object3) + objId3b = map.GetGlobalId(object3) + print('Object ids for object3: objId3 => ',objId3,', objId3b => ',objId3b) + self.assertTrue(objId3 == objId3b) + +if __name__ == "__main__": + Testing.main([(TestObjectId, 'test')]) diff --git a/Web/Core/Testing/Python/TestRemoteInteractionAdapter.py b/Web/Core/Testing/Python/TestRemoteInteractionAdapter.py new file mode 100644 index 000000000..f05c568c8 --- /dev/null +++ b/Web/Core/Testing/Python/TestRemoteInteractionAdapter.py @@ -0,0 +1,99 @@ +# SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen +# SPDX-License-Identifier: BSD-3-Clause + +""" Apply a series of events produced by vtk-js RenderWindowInteractor to a +vtkRenderwindow via the vtkRemoteInteractionAdapter class. The final image is +the expected scene after all interactions have been applied. +""" + +from vtkmodules.vtkRenderingCore import ( + vtkActor, + vtkPolyDataMapper, + vtkRenderer, + vtkRenderWindow, + vtkRenderWindowInteractor, +) +from vtkmodules.vtkWebCore import vtkRemoteInteractionAdapter +from vtkmodules.vtkFiltersSources import vtkConeSource + +from vtkmodules.test import Testing +import os +import json + +# Required for rendering initialization, +import vtkmodules.vtkRenderingOpenGL2 # noqa + +# Required for interactor initialization +from vtkmodules.vtkInteractionStyle import vtkInteractorStyleSwitch # noqa + + +# The scene to test. In some platforms reusing the window & renderer across +# the two test cases causes segfault. We start all tests from a clean state by +# creating the scene from scratch each time. +class Scene: + def __init__(self): + self.dataFile = os.path.join( + Testing.VTK_DATA_ROOT, "Data", "remote_events.json" + ) + self.imageFile = "TestRemoteInteractionAdapter.png" + self.adapter = vtkRemoteInteractionAdapter() + + print("dataFile: {}".format(self.dataFile)) + if not os.path.isfile(self.dataFile): + raise RuntimeError("Datafile is missing") + + self.renderer = vtkRenderer() + self.renderWindow = vtkRenderWindow() + self.renderWindow.AddRenderer(self.renderer) + self.renderWindow.SetSize(300, 300) + + self.renderWindowInteractor = vtkRenderWindowInteractor() + self.renderWindowInteractor.SetRenderWindow(self.renderWindow) + self.renderWindowInteractor.GetInteractorStyle().SetCurrentStyleToTrackballCamera() + + self.cone_source = vtkConeSource() + self.mapper = vtkPolyDataMapper() + self.mapper.SetInputConnection(self.cone_source.GetOutputPort()) + self.actor = vtkActor() + self.actor.SetMapper(self.mapper) + + self.renderer.AddActor(self.actor) + self.renderer.ResetCamera() + self.renderWindowInteractor.Initialize() + + +class TestRemoteInteractorAdapter(Testing.vtkTest): + def test0(self): + """Use class methods API for ProcessEvent""" + scene = Scene() + + adapter = vtkRemoteInteractionAdapter() + adapter.SetInteractor(scene.renderWindowInteractor) + + with open(scene.dataFile, "r") as f: + data = json.load(f) + for event in data["events"]: + event_str = json.dumps(event) + status = adapter.ProcessEvent(event_str) + assert status, f"Failed to process event\n {event_str}" + scene.renderWindowInteractor.Render() + self.assertImageMatch(scene.renderWindow, scene.imageFile) + + def test1(self): + """Use static method API for ProcessEvent""" + scene = Scene() + + with open(scene.dataFile, "r") as f: + data = json.load(f) + for event in data["events"]: + event_str = json.dumps(event) + status = vtkRemoteInteractionAdapter.ProcessEvent( + scene.renderWindowInteractor, event_str + ) + assert status, f"Failed to process event\n {event_str}" + scene.renderWindowInteractor.Render() + self.assertImageMatch(scene.renderWindow, scene.imageFile) + + +if __name__ == "__main__": + Testing.main([(TestRemoteInteractorAdapter, "test")]) diff --git a/Web/Core/Testing/Python/TestWebApplicationMemory.py b/Web/Core/Testing/Python/TestWebApplicationMemory.py new file mode 100644 index 000000000..cfac38f47 --- /dev/null +++ b/Web/Core/Testing/Python/TestWebApplicationMemory.py @@ -0,0 +1,42 @@ +from vtkmodules.vtkFiltersSources import vtkCylinderSource +from vtkmodules.vtkRenderingCore import ( + vtkActor, + vtkPolyDataMapper, + vtkRenderWindow, + vtkRenderer, +) +from vtkmodules.vtkWebCore import vtkWebApplication +import vtkmodules.vtkRenderingFreeType +import vtkmodules.vtkRenderingOpenGL2 +from vtkmodules.test import Testing +from vtkmodules.vtkWebCore import vtkWebApplication + +class TestWebApplicationMemory(Testing.vtkTest): + def testWebApplicationMemory(self): + cylinder = vtkCylinderSource() + cylinder.SetResolution(8) + + cylinderMapper = vtkPolyDataMapper() + cylinderMapper.SetInputConnection(cylinder.GetOutputPort()) + + cylinderActor = vtkActor() + cylinderActor.SetMapper(cylinderMapper) + cylinderActor.RotateX(30.0) + cylinderActor.RotateY(-45.0) + + ren = vtkRenderer() + renWin = vtkRenderWindow() + renWin.AddRenderer(ren) + ren.AddActor(cylinderActor) + renWin.SetSize(200, 200) + + ren.ResetCamera() + ren.GetActiveCamera().Zoom(1.5) + renWin.Render() + + webApp = vtkWebApplication() + # no memory leaks should be reported when compiling with VTK_DEBUG_LEAKS + webApp.StillRender(renWin) + +if __name__ == "__main__": + Testing.main([(TestWebApplicationMemory, 'test')]) diff --git a/Web/Core/vtk.module b/Web/Core/vtk.module new file mode 100644 index 000000000..f6a9e09a5 --- /dev/null +++ b/Web/Core/vtk.module @@ -0,0 +1,31 @@ +NAME + VTK::WebCore +LIBRARY_NAME + vtkWebCore +GROUPS + Web +SPDX_LICENSE_IDENTIFIER + BSD-3-Clause +SPDX_COPYRIGHT_TEXT + Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen +DEPENDS + VTK::CommonCore +PRIVATE_DEPENDS + VTK::CommonDataModel + VTK::CommonSystem + VTK::FiltersGeneral + VTK::FiltersGeometry + VTK::IOCore + VTK::IOImage + VTK::ParallelCore + VTK::Python + VTK::RenderingCore + VTK::WebGLExporter + VTK::vtksys + VTK::nlohmannjson +TEST_LABELS + VTK::Web +TEST_DEPENDS + VTK::ImagingCore + VTK::ImagingSources + VTK::TestingCore diff --git a/Web/Core/vtkDataEncoder.cxx b/Web/Core/vtkDataEncoder.cxx new file mode 100644 index 000000000..e93ca58f7 --- /dev/null +++ b/Web/Core/vtkDataEncoder.cxx @@ -0,0 +1,334 @@ +// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen +// SPDX-License-Identifier: BSD-3-Clause +#include "vtkDataEncoder.h" + +#include "vtkBase64Utilities.h" +#include "vtkCommand.h" +#include "vtkImageData.h" +#include "vtkJPEGWriter.h" +#include "vtkLogger.h" +#include "vtkNew.h" +#include "vtkObjectFactory.h" +#include "vtkPNGWriter.h" +#include "vtkSmartPointer.h" +#include "vtkUnsignedCharArray.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#define MAX_NUMBER_OF_THREADS_IN_POOL 32 + +namespace detail +{ +VTK_ABI_NAMESPACE_BEGIN + +struct vtkWork +{ + vtkSmartPointer Image; + int Quality = 0; + int Encoding = 0; + vtkTypeUInt64 TimeStamp = 0; + vtkTypeUInt32 Key = 0; + + vtkWork() = default; + vtkWork(vtkTypeUInt32 key, vtkImageData* image, int quality, int encoding) + : Image(image) + , Quality(quality) + , Encoding(encoding) + , TimeStamp(0) + , Key(key) + { + } + vtkWork(const vtkWork&) = default; + vtkWork& operator=(const vtkWork&) = default; +}; + +class vtkWorkQueue +{ + mutable std::mutex ResultsMutex; + std::map>> Results; + std::condition_variable ResultsCondition; + + std::map> LastTimeStamp; + + std::mutex QueueMutex; + std::queue Queue; + std::condition_variable QueueCondition; + + std::vector ThreadPool; + std::atomic Terminate; + + static void DoWork(int threadIndex, vtkWorkQueue* self) + { + vtkLogger::SetThreadName("Worker " + std::to_string(threadIndex)); + vtkLogF(TRACE, "starting worker thread"); + vtkNew writer; + writer->WriteToMemoryOn(); + while (!self->Terminate) + { + vtkWork work; + { + std::unique_lock lock(self->QueueMutex); + bool break_loop = false; + do + { + self->QueueCondition.wait_for(lock, std::chrono::seconds(1), + [self]() { return !self->Queue.empty() || self->Terminate; }); + if (self->Terminate) + { + break_loop = true; + break; + } + } while (self->Queue.empty()); + if (break_loop) + { + break; + } + work = self->Queue.front(); + self->Queue.pop(); + } + + writer->SetInputData(work.Image); + writer->SetQuality(work.Quality); + writer->Write(); + + auto result = vtkSmartPointer::New(); + if (work.Encoding) + { + vtkUnsignedCharArray* data = writer->GetResult(); + result->SetNumberOfComponents(1); + result->SetNumberOfTuples(std::ceil(1.5 * data->GetNumberOfTuples())); + unsigned long size = vtkBase64Utilities::Encode( + data->GetPointer(0), data->GetNumberOfTuples(), result->GetPointer(0), /*mark_end=*/0); + result->SetNumberOfTuples(static_cast(size) + 1); + result->SetValue(size, 0); + } + else + { + // We must do a deep copy here as the writer reuse that array + // and will change its values concurrently during its next job... + result->DeepCopy(writer->GetResult()); + } + writer->SetInputData(nullptr); + + { + std::unique_lock lock(self->ResultsMutex); + auto& pair = self->Results[work.Key]; + if (pair.first < work.TimeStamp) + { + pair = std::make_pair(work.TimeStamp, result); + lock.unlock(); + self->ResultsCondition.notify_all(); + } + } + } + + vtkLogF(TRACE, "exiting worker thread"); + } + +public: + vtkWorkQueue(int numThreads) + : Terminate(false) + { + assert(numThreads >= 0); + for (int cc = 0; cc < numThreads; ++cc) + { + this->ThreadPool.emplace_back(&vtkWorkQueue::DoWork, cc, this); + } + } + ~vtkWorkQueue() + { + this->Terminate = true; + this->QueueCondition.notify_all(); + for (auto& thread : this->ThreadPool) + { + thread.join(); + } + } + + bool IsValid() const { return !this->ThreadPool.empty(); } + + void PushBack(vtkWork&& work) + { + if (!this->IsValid()) + { + vtkLogF(ERROR, "Queue is invalid! Can't push work!"); + return; + } + + auto key = work.Key; + work.TimeStamp = ++this->LastTimeStamp[key]; + { + std::unique_lock lock(this->QueueMutex); + this->Queue.emplace(std::move(work)); + } + this->QueueCondition.notify_one(); + } + + bool GetResult(vtkTypeUInt32 key, vtkSmartPointer& data) const + { + std::unique_lock lock(this->ResultsMutex); + auto iter = this->Results.find(key); + if (iter == this->Results.end()) + { + return false; + } + + const auto& resultsPair = iter->second; + data = resultsPair.second; + // return true if this is the latest result for this key. + return (resultsPair.first == this->LastTimeStamp.at(key)); + } + + void Flush(vtkTypeUInt32 key) + { + auto tsIter = this->LastTimeStamp.find(key); + if (tsIter == this->LastTimeStamp.end()) + { + return; + } + const auto& ts = tsIter->second; + std::unique_lock lock(this->ResultsMutex); + this->ResultsCondition.wait(lock, + [this, &ts, &key]() + { + try + { + return ts == this->Results[key].first; + } + catch (std::out_of_range&) + { + // result not available yet; keep waiting; + return false; + } + }); + } +}; +VTK_ABI_NAMESPACE_END +} // namespace detail + +VTK_ABI_NAMESPACE_BEGIN +//**************************************************************************** +class vtkDataEncoder::vtkInternals +{ +public: + detail::vtkWorkQueue Queue; + vtkNew LastBase64Image; + + vtkInternals(int numThreads) + : Queue(numThreads) + { + } + + // Once an imagedata has been written to memory as a jpg or png, this + // convenience function can encode that image as a Base64 string. + const char* GetBase64EncodedImage(vtkUnsignedCharArray* encodedInputImage) + { + this->LastBase64Image->SetNumberOfComponents(1); + this->LastBase64Image->SetNumberOfTuples( + std::ceil(1.5 * encodedInputImage->GetNumberOfTuples())); + unsigned long size = vtkBase64Utilities::Encode(encodedInputImage->GetPointer(0), + encodedInputImage->GetNumberOfTuples(), this->LastBase64Image->GetPointer(0), /*mark_end=*/0); + + this->LastBase64Image->SetNumberOfTuples(static_cast(size) + 1); + this->LastBase64Image->SetValue(size, 0); + + return reinterpret_cast(this->LastBase64Image->GetPointer(0)); + } +}; + +vtkStandardNewMacro(vtkDataEncoder); +//------------------------------------------------------------------------------ +vtkDataEncoder::vtkDataEncoder() + : MaxThreads(3) + , Internals(new vtkInternals(this->MaxThreads)) +{ +} + +//------------------------------------------------------------------------------ +vtkDataEncoder::~vtkDataEncoder() = default; + +//------------------------------------------------------------------------------ +void vtkDataEncoder::SetMaxThreads(vtkTypeUInt32 maxThreads) +{ + if (maxThreads < MAX_NUMBER_OF_THREADS_IN_POOL && maxThreads > 0) + { + this->MaxThreads = maxThreads; + } +} + +//------------------------------------------------------------------------------ +void vtkDataEncoder::Initialize() +{ + this->Internals.reset(new vtkDataEncoder::vtkInternals(this->MaxThreads)); +} + +//------------------------------------------------------------------------------ +void vtkDataEncoder::Push(vtkTypeUInt32 key, vtkImageData* data, int quality, int encoding) +{ + auto& internals = (*this->Internals); + internals.Queue.PushBack(detail::vtkWork(key, data, quality, encoding)); +} + +//------------------------------------------------------------------------------ +bool vtkDataEncoder::GetLatestOutput(vtkTypeUInt32 key, vtkSmartPointer& data) +{ + auto& internals = (*this->Internals); + return internals.Queue.GetResult(key, data); +} + +//------------------------------------------------------------------------------ +const char* vtkDataEncoder::EncodeAsBase64Png(vtkImageData* img, int compressionLevel) +{ + // Perform in-memory write of image as png + vtkNew writer; + writer->WriteToMemoryOn(); + writer->SetInputData(img); + writer->SetCompressionLevel(compressionLevel); + writer->Write(); + + // Return Base64-encoded string + return this->Internals->GetBase64EncodedImage(writer->GetResult()); +} + +//------------------------------------------------------------------------------ +const char* vtkDataEncoder::EncodeAsBase64Jpg(vtkImageData* img, int quality) +{ + // Perform in-memory write of image as jpg + vtkNew writer; + writer->WriteToMemoryOn(); + writer->SetInputData(img); + writer->SetQuality(quality); + writer->Write(); + + // Return Base64-encoded string + return this->Internals->GetBase64EncodedImage(writer->GetResult()); +} + +//------------------------------------------------------------------------------ +void vtkDataEncoder::Flush(vtkTypeUInt32 key) +{ + auto& internals = (*this->Internals); + internals.Queue.Flush(key); +} + +//------------------------------------------------------------------------------ +void vtkDataEncoder::PrintSelf(ostream& os, vtkIndent indent) +{ + this->Superclass::PrintSelf(os, indent); +} + +//------------------------------------------------------------------------------ +void vtkDataEncoder::Finalize() +{ + this->Internals.reset(new vtkDataEncoder::vtkInternals(0)); +} +VTK_ABI_NAMESPACE_END diff --git a/Web/Core/vtkDataEncoder.h b/Web/Core/vtkDataEncoder.h new file mode 100644 index 000000000..2fbc7ea53 --- /dev/null +++ b/Web/Core/vtkDataEncoder.h @@ -0,0 +1,110 @@ +// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen +// SPDX-License-Identifier: BSD-3-Clause +/** + * @class vtkDataEncoder + * @brief class used to compress/encode images using threads. + * + * vtkDataEncoder is used to compress and encode images using threads. + * Multiple images can be pushed into the encoder for compression and encoding. + * We use a vtkTypeUInt32 as the key to identify different image pipes. The + * images in each pipe will be processed in parallel threads. The latest + * compressed and encoded image can be accessed using GetLatestOutput(). + * + * vtkDataEncoder uses a thread-pool to do the compression and encoding in + * parallel. Note that images may not come out of the vtkDataEncoder in the + * same order as they are pushed in, if an image pushed in at N-th location + * takes longer to compress and encode than that pushed in at N+1-th location or + * if it was pushed in before the N-th location was even taken up for encoding + * by the a thread in the thread pool. + */ + +#ifndef vtkDataEncoder_h +#define vtkDataEncoder_h + +#include "vtkObject.h" +#include "vtkSmartPointer.h" // needed for vtkSmartPointer +#include "vtkWebCoreModule.h" // needed for exports +#include // for std::unique_ptr + +VTK_ABI_NAMESPACE_BEGIN +class vtkUnsignedCharArray; +class vtkImageData; + +class VTKWEBCORE_EXPORT vtkDataEncoder : public vtkObject +{ +public: + static vtkDataEncoder* New(); + vtkTypeMacro(vtkDataEncoder, vtkObject); + void PrintSelf(ostream& os, vtkIndent indent) override; + + ///@{ + /** + * Define the number of worker threads to use. Default is 3. + * Initialize() needs to be called after changing the thread count. + */ + void SetMaxThreads(vtkTypeUInt32); + vtkGetMacro(MaxThreads, vtkTypeUInt32); + ///@} + + /** + * Re-initializes the encoder. This will abort any on going encoding threads + * and clear internal data-structures. + */ + void Initialize(); + + /** + * Push an image into the encoder. The data is considered unchanging and thus + * should not be modified once pushed. Reference count changes are now thread safe + * and hence callers should ensure they release the reference held, if + * appropriate. + */ + void Push(vtkTypeUInt32 key, vtkImageData* data, int quality, int encoding = 1); + + /** + * Get access to the most-recent fully encoded result corresponding to the + * given key, if any. This methods returns true if the \c data obtained is the + * result from the most recent Push() for the key, if any. If this method + * returns false, it means that there's some image either being processed on + * pending processing. + */ + bool GetLatestOutput(vtkTypeUInt32 key, vtkSmartPointer& data); + + /** + * Flushes the encoding pipe and blocks till the most recently pushed image + * for the particular key has been processed. This call will block. Once this + * method returns, caller can use GetLatestOutput(key) to access the processed + * output. + */ + void Flush(vtkTypeUInt32 key); + + /** + * Take an image data and synchronously convert it to a base-64 encoded png. + */ + const char* EncodeAsBase64Png(vtkImageData* img, int compressionLevel = 5); + + /** + * Take an image data and synchronously convert it to a base-64 encoded jpg. + */ + const char* EncodeAsBase64Jpg(vtkImageData* img, int quality = 50); + + /** + * This method will wait for any running thread to terminate. + */ + void Finalize(); + +protected: + vtkDataEncoder(); + ~vtkDataEncoder() override; + + vtkTypeUInt32 MaxThreads; + +private: + vtkDataEncoder(const vtkDataEncoder&) = delete; + void operator=(const vtkDataEncoder&) = delete; + + class vtkInternals; + std::unique_ptr Internals; +}; + +VTK_ABI_NAMESPACE_END +#endif diff --git a/Web/Core/vtkObjectIdMap.cxx b/Web/Core/vtkObjectIdMap.cxx new file mode 100644 index 000000000..bd676cef8 --- /dev/null +++ b/Web/Core/vtkObjectIdMap.cxx @@ -0,0 +1,123 @@ +// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen +// SPDX-License-Identifier: BSD-3-Clause +#include "vtkObjectIdMap.h" + +#include "vtkObjectFactory.h" +#include "vtkSmartPointer.h" +#include "vtkWeakPointer.h" + +#include +#include +#include + +VTK_ABI_NAMESPACE_BEGIN +struct vtkObjectIdMap::vtkInternals +{ + std::map> Object; + std::map, vtkTypeUInt32> GlobalId; + std::map> ActiveObjects; + vtkTypeUInt32 NextAvailableId; + + vtkInternals() + : NextAvailableId(1) + { + } +}; + +vtkStandardNewMacro(vtkObjectIdMap); +//------------------------------------------------------------------------------ +vtkObjectIdMap::vtkObjectIdMap() + : Internals(new vtkInternals()) +{ +} + +//------------------------------------------------------------------------------ +vtkObjectIdMap::~vtkObjectIdMap() +{ + delete this->Internals; + this->Internals = nullptr; +} + +//------------------------------------------------------------------------------ +void vtkObjectIdMap::PrintSelf(ostream& os, vtkIndent indent) +{ + this->Superclass::PrintSelf(os, indent); +} + +//------------------------------------------------------------------------------ +vtkTypeUInt32 vtkObjectIdMap::GetGlobalId(vtkObject* obj) +{ + if (obj == nullptr) + { + return 0; + } + + auto iter = this->Internals->GlobalId.find(obj); + if (iter == this->Internals->GlobalId.end()) + { + vtkTypeUInt32 globalId = this->Internals->NextAvailableId++; + this->Internals->GlobalId[obj] = globalId; + this->Internals->Object[globalId] = obj; + return globalId; + } + return iter->second; +} + +//------------------------------------------------------------------------------ +vtkObject* vtkObjectIdMap::GetVTKObject(vtkTypeUInt32 globalId) +{ + auto iter = this->Internals->Object.find(globalId); + if (iter == this->Internals->Object.end()) + { + return nullptr; + } + return iter->second; +} + +//------------------------------------------------------------------------------ +vtkTypeUInt32 vtkObjectIdMap::SetActiveObject(const char* objectType, vtkObject* obj) +{ + if (objectType) + { + this->Internals->ActiveObjects[objectType] = obj; + return this->GetGlobalId(obj); + } + return 0; +} + +//------------------------------------------------------------------------------ +vtkObject* vtkObjectIdMap::GetActiveObject(const char* objectType) +{ + if (objectType) + { + return this->Internals->ActiveObjects[objectType]; + } + return nullptr; +} + +//------------------------------------------------------------------------------ +bool vtkObjectIdMap::FreeObject(vtkObject* obj) +{ + auto iter = this->Internals->GlobalId.find(obj); + auto found = iter != this->Internals->GlobalId.end(); + if (found) + { + this->Internals->Object.erase(iter->second); + this->Internals->GlobalId.erase(iter); + } + return found; +} + +//------------------------------------------------------------------------------ +bool vtkObjectIdMap::FreeObjectById(vtkTypeUInt32 id) +{ + auto iter = this->Internals->Object.find(id); + auto found = iter != this->Internals->Object.end(); + if (found) + { + this->Internals->GlobalId.erase(iter->second); + this->Internals->Object.erase(iter); + } + return found; +} +VTK_ABI_NAMESPACE_END diff --git a/Web/Core/vtkObjectIdMap.h b/Web/Core/vtkObjectIdMap.h new file mode 100644 index 000000000..2f7852148 --- /dev/null +++ b/Web/Core/vtkObjectIdMap.h @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen +// SPDX-License-Identifier: BSD-3-Clause +/** + * @class vtkObjectIdMap + * @brief class used to assign Id to any VTK object and be able + * to retrieve it base on its id. + */ + +#ifndef vtkObjectIdMap_h +#define vtkObjectIdMap_h + +#include "vtkObject.h" +#include "vtkWebCoreModule.h" // needed for exports + +VTK_ABI_NAMESPACE_BEGIN +class VTKWEBCORE_EXPORT vtkObjectIdMap : public vtkObject +{ +public: + static vtkObjectIdMap* New(); + vtkTypeMacro(vtkObjectIdMap, vtkObject); + void PrintSelf(ostream& os, vtkIndent indent) override; + + /** + * Retrieve a unique identifier for the given object or generate a new one + * if its global id was never requested. + */ + vtkTypeUInt32 GetGlobalId(vtkObject* obj); + + /** + * Retrieve a vtkObject based on its global id. If not found return nullptr + */ + vtkObject* GetVTKObject(vtkTypeUInt32 globalId); + + /** + * Assign an active key (string) to an existing object. + * This is usually used to provide another type of access to specific + * vtkObject that we want to retrieve easily using a string. + * Return the global Id of the given registered object + */ + vtkTypeUInt32 SetActiveObject(const char* objectType, vtkObject* obj); + + /** + * Retrieve a previously stored object based on a name + */ + vtkObject* GetActiveObject(const char* objectType); + + /** + * Given an object, remove any internal reference count due to + * internal Id/Object mapping. + * Returns true if the item existed in the map and was deleted. + */ + bool FreeObject(vtkObject* obj); + + /** + * Given an id, remove any internal reference count due to + * internal Id/Object mapping. + * Returns true if the id existed in the map and was deleted. + */ + bool FreeObjectById(vtkTypeUInt32 id); + +protected: + vtkObjectIdMap(); + ~vtkObjectIdMap() override; + +private: + vtkObjectIdMap(const vtkObjectIdMap&) = delete; + void operator=(const vtkObjectIdMap&) = delete; + + struct vtkInternals; + vtkInternals* Internals; +}; + +VTK_ABI_NAMESPACE_END +#endif diff --git a/Web/Core/vtkRemoteInteractionAdapter.cxx b/Web/Core/vtkRemoteInteractionAdapter.cxx new file mode 100644 index 000000000..a4167cdf1 --- /dev/null +++ b/Web/Core/vtkRemoteInteractionAdapter.cxx @@ -0,0 +1,291 @@ +// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen +// SPDX-License-Identifier: BSD-3-Clause + +#include "vtkRemoteInteractionAdapter.h" +#include "vtkCommand.h" +#include "vtkLogger.h" +#include "vtkObjectFactory.h" +#include "vtkRenderWindow.h" +#include "vtkRenderWindowInteractor.h" + +#include +#include VTK_NLOHMANN_JSON(json.hpp) + +#include + +VTK_ABI_NAMESPACE_BEGIN + +enum +{ + WheelEvent = vtkCommand::UserEvent + 3000, +}; + +// map vtk-js event codes to vtkCommand events , for the ones that I didn't found a clear +// correspondence I used vtkCommand::NoEvent and left them unhandled. Taken from +// https://github.com/Kitware/vtk-js/blob/master/Sources/Rendering/Core/RenderWindowInteractor/index.js + +using enum_type = int; +// clang-format off +const std::unordered_map< std::string, enum_type > EVENT_MAP { + {"StartAnimation" ,vtkCommand::NoEvent}, + {"Animation" ,vtkCommand::NoEvent}, + {"EndAnimation" ,vtkCommand::NoEvent}, + {"PointerEnter" ,vtkCommand::EnterEvent}, + {"PointerLeave" ,vtkCommand::LeaveEvent}, + {"MouseEnter" ,vtkCommand::EnterEvent}, + {"MouseLeave" ,vtkCommand::LeaveEvent}, + {"StartMouseMove" ,vtkCommand::NoEvent}, + {"MouseMove" ,vtkCommand::MouseMoveEvent}, + {"EndMouseMove" ,vtkCommand::NoEvent}, + {"LeftButtonPress" ,vtkCommand::LeftButtonPressEvent}, + {"LeftButtonRelease" ,vtkCommand::LeftButtonReleaseEvent}, + {"MiddleButtonPress" ,vtkCommand::MiddleButtonPressEvent}, + {"MiddleButtonRelease" ,vtkCommand::MiddleButtonReleaseEvent}, + {"RightButtonPress" ,vtkCommand::RightButtonPressEvent}, + {"RightButtonRelease" ,vtkCommand::RightButtonReleaseEvent}, + {"KeyPress" ,vtkCommand::KeyPressEvent}, + {"KeyDown" ,vtkCommand::KeyPressEvent}, + {"KeyUp" ,vtkCommand::KeyReleaseEvent}, + {"StartMouseWheel" ,vtkCommand::NoEvent}, + {"MouseWheel" ,WheelEvent}, + {"EndMouseWheel" ,vtkCommand::NoEvent}, + {"StartPinch" ,vtkCommand::StartPinchEvent}, + {"Pinch" ,vtkCommand::PinchEvent}, + {"EndPinch" ,vtkCommand::EndPinchEvent}, + {"StartPan" ,vtkCommand::StartPanEvent}, + {"Pan" ,vtkCommand::PanEvent}, + {"EndPan" ,vtkCommand::EndPanEvent}, + {"StartRotate" ,vtkCommand::StartRotateEvent}, + {"Rotate" ,vtkCommand::RotateEvent}, + {"EndRotate" ,vtkCommand::RenderEvent}, + {"Button3D" ,vtkCommand::NoEvent}, + {"Move3D" ,vtkCommand::NoEvent}, + {"StartPointerLock" ,vtkCommand::NoEvent}, + {"EndPointerLock" ,vtkCommand::NoEvent}, + {"StartInteraction" ,vtkCommand::NoEvent}, + {"Interaction" ,vtkCommand::NoEvent}, + {"EndInteraction" ,vtkCommand::NoEvent}, + {"AnimationFrameRateUpdate" ,vtkCommand::NoEvent} +}; +// clang-format on + +vtkSetObjectImplementationMacro(vtkRemoteInteractionAdapter, Interactor, vtkRenderWindowInteractor); +//---------------------------------------------------------------------------- +vtkStandardNewMacro(vtkRemoteInteractionAdapter); + +//---------------------------------------------------------------------------- +vtkRemoteInteractionAdapter::vtkRemoteInteractionAdapter() = default; + +//---------------------------------------------------------------------------- +vtkRemoteInteractionAdapter::~vtkRemoteInteractionAdapter() +{ + this->SetInteractor(nullptr); +} + +//---------------------------------------------------------------------------- +void vtkRemoteInteractionAdapter::PrintSelf(ostream& os, vtkIndent indent) +{ + this->Superclass::PrintSelf(os, indent); +} + +//---------------------------------------------------------------------------- +// based on QVTKInteractorAdapter::ProcessEvent(QEvent* e, vtkRenderWindowInteractor* iren) +bool vtkRemoteInteractionAdapter::ProcessEvent(vtkRenderWindowInteractor* iren, + const std::string& event_str, double devicePixelRatio, double devicePixelRatioTolerance) +{ + + if (!iren) + { + vtkLogF(ERROR, "Null interactor passed"); + return false; + } + // the following events only happen if the interactor is enabled + if (!iren->GetEnabled()) + { + return false; + } + + try + { + nlohmann::json event = nlohmann::json::parse(event_str); + const std::string& type = event.at("type"); + vtkLogF(TRACE, "event %s", event.dump(1).c_str()); + const int eventType = EVENT_MAP.at(type); + switch (eventType) + { + case vtkCommand::EnterEvent: + case vtkCommand::LeaveEvent: + iren->InvokeEvent(eventType, (void*)&event); + break; + case vtkCommand::MouseMoveEvent: + case vtkCommand::LeftButtonPressEvent: + case vtkCommand::LeftButtonReleaseEvent: + case vtkCommand::RightButtonPressEvent: + case vtkCommand::RightButtonReleaseEvent: + case vtkCommand::MiddleButtonPressEvent: + case vtkCommand::MiddleButtonReleaseEvent: + { + const int x = + (event.at("x").get() / event.at("w").get() * devicePixelRatio + + devicePixelRatioTolerance) * + iren->GetRenderWindow()->GetSize()[0]; + const int y = + (event.at("y").get() / event.at("h").get() * devicePixelRatio + + devicePixelRatioTolerance) * + iren->GetRenderWindow()->GetSize()[1]; + + const int ctrlKeyPressed = event.at("ctrlKey").get(); + const int altKeyPressed = event.at("altKey").get(); + const int shiftKeyPressed = event.at("shiftKey").get(); + iren->SetEventInformation(x, y, ctrlKeyPressed, shiftKeyPressed); + iren->SetAltKey(altKeyPressed); + iren->InvokeEvent(eventType, (void*)&event); + break; + } + case vtkCommand::KeyPressEvent: + case vtkCommand::KeyReleaseEvent: + { + const int ctrlKeyPressed = event.at("controlKey").get(); + const int altKeyPressed = event.at("altKey").get(); + const int shiftKeyPressed = event.at("shiftKey").get(); + const char asciiCode = event.at("keyCode").get(); + const std::string& key = event.at("key"); + iren->SetKeyEventInformation(ctrlKeyPressed, shiftKeyPressed, asciiCode, 0, key.c_str()); + iren->SetAltKey(altKeyPressed); + iren->InvokeEvent(eventType); + if (eventType == vtkCommand::KeyPressEvent && asciiCode != '\0') // TODO check comparson + { + iren->InvokeEvent(vtkCommand::CharEvent, (void*)&event); + } + break; + } + case WheelEvent: + { + const int x = + (event.at("x").get() / event.at("w").get() * devicePixelRatio + + devicePixelRatioTolerance) * + iren->GetRenderWindow()->GetSize()[0]; + const int y = + (event.at("y").get() / event.at("h").get() * devicePixelRatio + + devicePixelRatioTolerance) * + iren->GetRenderWindow()->GetSize()[1]; + + const int ctrlKeyPressed = event.at("ctrlKey").get(); + const int altKeyPressed = event.at("altKey").get(); + const int shiftKeyPressed = event.at("shiftKey").get(); + + iren->SetEventInformation(x, y, ctrlKeyPressed, shiftKeyPressed); + iren->SetAltKey(altKeyPressed); + + static double accumulatedDelta = 0; + const double verticalDelta = event.at("spinY").get(); + accumulatedDelta += verticalDelta; + const double threshold = 1.0; // in vtk-js the value comes normalized + + // invoke vtk event when accumulated delta passes the threshold + // Note: in javascript a forward (away from the user MouseWheelEvent is + // indicated with a negative value in contrast to Qt. + if (accumulatedDelta <= -threshold && verticalDelta != 0.0) + { + iren->InvokeEvent(vtkCommand::MouseWheelForwardEvent, (void*)&event); + accumulatedDelta = 0; + } + else if (accumulatedDelta >= threshold && verticalDelta != 0.0) + { + iren->InvokeEvent(vtkCommand::MouseWheelBackwardEvent, (void*)&event); + accumulatedDelta = 0; + } + + break; + } + case vtkCommand::StartPinchEvent: + case vtkCommand::EndPinchEvent: + case vtkCommand::PinchEvent: + case vtkCommand::StartPanEvent: + case vtkCommand::EndPanEvent: + case vtkCommand::PanEvent: + case vtkCommand::StartRotateEvent: + case vtkCommand::EndRotateEvent: + case vtkCommand::RotateEvent: + { + // Store event information to restore after gesture is completed + int eventPosition[2]; + iren->GetEventPosition(eventPosition); + int lastEventPosition[2]; + iren->GetLastEventPosition(lastEventPosition); + + // get center of positions for event + int position[2] = { 0, 0 }; + for (const auto& item : event.at("positions")) + { + position[0] += item.at("x").get() / event.at("w").get(); + position[1] += item.at("y").get() / event.at("h").get(); + } + + position[0] /= static_cast(event.at("positions").size()); + position[1] /= static_cast(event.at("positions").size()); + + iren->SetEventInformation(position[0] * devicePixelRatio * devicePixelRatioTolerance, + position[1] * devicePixelRatio * devicePixelRatioTolerance); + + if (eventType == vtkCommand::StartPinchEvent || eventType == vtkCommand::EndPinchEvent || + eventType == vtkCommand::PinchEvent) + { + const double factor = event.at("factor").get(); + iren->SetScale(1.0); + iren->SetScale(factor); + } + else if (eventType == vtkCommand::StartPanEvent || eventType == vtkCommand::EndPanEvent || + eventType == vtkCommand::PanEvent) + { + double translation[2] = { event.at("translation").at(0).get(), + event.at("translation").at(1).get() }; + iren->SetTranslation(translation); + } + else if (eventType == vtkCommand::StartRotateEvent || + eventType == vtkCommand::EndRotateEvent || eventType == vtkCommand::RotateEvent) + { + const double rotation = event.at("rotation").get(); + iren->SetRotation(rotation); + } + else + { + vtkLogF(ERROR, "Unexpected Event Type"); + return false; + } + iren->InvokeEvent(eventType, (void*)&event); + } + break; + + case vtkCommand::InteractionEvent: + case vtkCommand::StartInteractionEvent: + case vtkCommand::EndInteractionEvent: + case vtkCommand::NoEvent: + // nothing to do + break; + default: + vtkLogF(WARNING, "Unhandled event: %s", type.c_str()); + break; + } + return true; + } + catch (std::out_of_range& e) + { + vtkLogF(ERROR, "Skipping Event: Unknown event type \n%s", e.what()); + return false; + } + catch (nlohmann::json::out_of_range& e) + { + vtkLogF(ERROR, "Skipping Event \n%s", e.what()); + return false; + } +} + +//---------------------------------------------------------------------------- +bool vtkRemoteInteractionAdapter::ProcessEvent(const std::string& event_str) +{ + return ProcessEvent( + this->Interactor, event_str, this->DevicePixelRatio, this->DevicePixelRatioTolerance); +} + +VTK_ABI_NAMESPACE_END diff --git a/Web/Core/vtkRemoteInteractionAdapter.h b/Web/Core/vtkRemoteInteractionAdapter.h new file mode 100644 index 000000000..e2cb178e4 --- /dev/null +++ b/Web/Core/vtkRemoteInteractionAdapter.h @@ -0,0 +1,85 @@ +// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen +// SPDX-License-Identifier: BSD-3-Clause + +/** + * @class vtkRemoteInteractionAdapter + * @brief Map vtk-js interaction events to native VTK events + * + * Apply an vtk-js events to a vtkRenderWindowInteractor. + * For the expected format see + * https://github.com/Kitware/vtk-js/blob/master/Sources/Interaction/Style/InteractorStyleRemoteMouse/index.js + * + * Events are processed in the `ProcessEvent` method which can be called + * either as a static method providing all the relevant parameters as arguments + * or a class method with the parameters provided via member variables. + * + */ + +#ifndef vtkRemoteInteractionAdapter_h +#define vtkRemoteInteractionAdapter_h + +#include "vtkObject.h" +#include "vtkWebCoreModule.h" // for exports + +VTK_ABI_NAMESPACE_BEGIN + +class vtkRenderWindowInteractor; + +class VTKWEBCORE_EXPORT vtkRemoteInteractionAdapter : public vtkObject +{ +public: + static vtkRemoteInteractionAdapter* New(); + vtkTypeMacro(vtkRemoteInteractionAdapter, vtkObject); + void PrintSelf(ostream& os, vtkIndent indent) override; + + /** + * @brief Apply the vtk-js event to the internal RenderWindowInteractor + * @param event stringified json representation of a vtk-js interaction event. + * @return true if the event is processed , false otherwise + */ + bool ProcessEvent(const std::string& event); + + /** + * Static version of ProcessEvent(const std::string&) + * @return true if the event is processed , false otherwise + */ + static bool ProcessEvent(vtkRenderWindowInteractor* iren, const std::string& event, + double devicePixelRatio = 1.0, double devicePixelRatioTolerance = 1e-5); + + ///@{ + // Get/Set the ratio between physical (onscreen) pixel and logical (rendered image) + vtkSetMacro(DevicePixelRatio, double); + vtkGetMacro(DevicePixelRatio, double); + ///@} + + ///@{ + /** + * Tolerance used when truncating the event position from + * physical to logical. i.e. int event_position_x = int(event.at("x") * devicePixelRatio + + * devicePixelRatioTolerance) + */ + vtkSetMacro(DevicePixelRatioTolerance, double); + vtkGetMacro(DevicePixelRatioTolerance, double); + ///@} + + ///@{ + // Get/Set the Interactor to apply the event to. + void SetInteractor(vtkRenderWindowInteractor* iren); + vtkGetObjectMacro(Interactor, vtkRenderWindowInteractor); + ///@} + +protected: + vtkRemoteInteractionAdapter(); + ~vtkRemoteInteractionAdapter() override; + +private: + vtkRemoteInteractionAdapter(const vtkRemoteInteractionAdapter&) = delete; + void operator=(const vtkRemoteInteractionAdapter&) = delete; + + double DevicePixelRatio = 1.0; + double DevicePixelRatioTolerance = 1e-5; + vtkRenderWindowInteractor* Interactor = nullptr; +}; + +VTK_ABI_NAMESPACE_END +#endif diff --git a/Web/Core/vtkWebApplication.cxx b/Web/Core/vtkWebApplication.cxx new file mode 100644 index 000000000..a8bacb551 --- /dev/null +++ b/Web/Core/vtkWebApplication.cxx @@ -0,0 +1,464 @@ +// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen +// SPDX-License-Identifier: BSD-3-Clause +#include "vtkWebApplication.h" + +#include "vtkBase64Utilities.h" +#include "vtkCamera.h" +#include "vtkCommand.h" +#include "vtkDataEncoder.h" +#include "vtkImageData.h" +#include "vtkJPEGWriter.h" +#include "vtkNew.h" +#include "vtkObjectFactory.h" +#include "vtkObjectIdMap.h" +#include "vtkPNGWriter.h" +#include "vtkPointData.h" +#include "vtkRenderWindow.h" +#include "vtkRenderWindowInteractor.h" +#include "vtkRendererCollection.h" +#include "vtkSmartPointer.h" +#include "vtkTimerLog.h" +#include "vtkUnsignedCharArray.h" +#include "vtkWebGLExporter.h" +#include "vtkWebGLObject.h" +#include "vtkWebInteractionEvent.h" +#include "vtkWindowToImageFilter.h" + +#include +#include +#include +#include + +VTK_ABI_NAMESPACE_BEGIN +class vtkWebApplication::vtkInternals +{ +public: + struct ImageCacheValueType + { + public: + vtkSmartPointer Data; + bool NeedsRender; + bool HasImagesBeingProcessed; + vtkObject* ViewPointer; + unsigned long ObserverId; + ImageCacheValueType() + : NeedsRender(true) + , HasImagesBeingProcessed(false) + , ViewPointer(nullptr) + , ObserverId(0) + { + } + + void SetListener(vtkObject* view) + { + if (this->ViewPointer == view) + { + return; + } + + if (this->ViewPointer && this->ObserverId) + { + this->ViewPointer->RemoveObserver(this->ObserverId); + this->ObserverId = 0; + } + this->ViewPointer = view; + if (this->ViewPointer) + { + this->ObserverId = this->ViewPointer->AddObserver( + vtkCommand::AnyEvent, this, &ImageCacheValueType::ViewEventListener); + } + } + + void RemoveListener(vtkObject* view) + { + if (this->ViewPointer && this->ViewPointer == view && this->ObserverId) + { + this->ViewPointer->RemoveObserver(this->ObserverId); + this->ObserverId = 0; + this->ViewPointer = nullptr; + } + } + + void ViewEventListener(vtkObject*, unsigned long, void*) { this->NeedsRender = true; } + }; + typedef std::map ImageCacheType; + ImageCacheType ImageCache; + + typedef std::map ButtonStatesType; + ButtonStatesType ButtonStates; + + vtkNew Encoder; + + // WebGL related struct + struct WebGLObjCacheValue + { + public: + int ObjIndex; + std::map BinaryParts; + }; + // map for > + typedef std::map WebGLObjId2IndexMap; + std::map WebGLExporterObjIdMap; + // map for + std::map> ViewWebGLMap; + std::string LastAllWebGLBinaryObjects; + vtkNew ObjectIdMap; +}; + +vtkStandardNewMacro(vtkWebApplication); +//------------------------------------------------------------------------------ +vtkWebApplication::vtkWebApplication() + : ImageEncoding(ENCODING_BASE64) + , ImageCompression(COMPRESSION_JPEG) + , Internals(new vtkWebApplication::vtkInternals()) +{ +} + +//------------------------------------------------------------------------------ +vtkWebApplication::~vtkWebApplication() +{ + delete this->Internals; + this->Internals = nullptr; +} + +//------------------------------------------------------------------------------ +void vtkWebApplication::SetNumberOfEncoderThreads(vtkTypeUInt32 numThreads) +{ + this->Internals->Encoder->SetMaxThreads(numThreads); + this->Internals->Encoder->Initialize(); +} + +//------------------------------------------------------------------------------ +vtkTypeUInt32 vtkWebApplication::GetNumberOfEncoderThreads() +{ + return this->Internals->Encoder->GetMaxThreads(); +} + +//------------------------------------------------------------------------------ +bool vtkWebApplication::GetHasImagesBeingProcessed(vtkRenderWindow* view) +{ + const vtkInternals::ImageCacheValueType& value = this->Internals->ImageCache[view]; + return value.HasImagesBeingProcessed; +} + +//------------------------------------------------------------------------------ +vtkUnsignedCharArray* vtkWebApplication::InteractiveRender(vtkRenderWindow* view, int quality) +{ + // for now, just do the same as StillRender(). + return this->StillRender(view, quality); +} + +//------------------------------------------------------------------------------ +void vtkWebApplication::InvalidateCache(vtkRenderWindow* view) +{ + this->Internals->ImageCache[view].NeedsRender = true; +} + +//------------------------------------------------------------------------------ +vtkUnsignedCharArray* vtkWebApplication::StillRender(vtkRenderWindow* view, int quality) +{ + if (!view) + { + vtkErrorMacro("No view specified."); + return nullptr; + } + + auto viewID = this->Internals->ObjectIdMap->GetGlobalId(view); + vtkInternals::ImageCacheValueType& value = this->Internals->ImageCache[view]; + value.SetListener(view); + + if (!value.NeedsRender && + value.Data != nullptr /* FIXME SEB && + view->HasDirtyRepresentation() == false */) + { + bool latest = this->Internals->Encoder->GetLatestOutput(viewID, value.Data); + value.HasImagesBeingProcessed = !latest; + return value.Data; + } + + // cout << "Regenerating " << endl; + // vtkTimerLog::ResetLog(); + // vtkTimerLog::CleanupLog(); + // vtkTimerLog::MarkStartEvent("StillRenderToString"); + // vtkTimerLog::MarkStartEvent("CaptureWindow"); + + view->Render(); + + // TODO: We should add logic to check if a new rendering needs to be done and + // then alone do a new rendering otherwise use the cached image. + vtkNew w2i; + w2i->SetInput(view); + w2i->SetScale(1); + w2i->ReadFrontBufferOff(); + w2i->ShouldRerenderOff(); + w2i->FixBoundaryOn(); + w2i->Update(); + + auto image = vtkSmartPointer::New(); + image->ShallowCopy(w2i->GetOutput()); + + // vtkTimerLog::MarkEndEvent("CaptureWindow"); + + // vtkTimerLog::MarkEndEvent("StillRenderToString"); + // vtkTimerLog::DumpLogWithIndents(&cout, 0.0); + + this->Internals->Encoder->Push(viewID, image, quality, this->ImageEncoding); + + if (value.Data == nullptr) + { + // we need to wait till output is processed. + // cout << "Flushing" << endl; + this->Internals->Encoder->Flush(viewID); + // cout << "Done Flushing" << endl; + } + + bool latest = this->Internals->Encoder->GetLatestOutput(viewID, value.Data); + value.HasImagesBeingProcessed = !latest; + value.NeedsRender = false; + return value.Data; +} + +//------------------------------------------------------------------------------ +const char* vtkWebApplication::StillRenderToString( + vtkRenderWindow* view, vtkMTimeType time, int quality) +{ + vtkUnsignedCharArray* array = this->StillRender(view, quality); + if (array && array->GetMTime() != time) + { + this->LastStillRenderToMTime = array->GetMTime(); + // cout << "Image size: " << array->GetNumberOfTuples() << endl; + return reinterpret_cast(array->GetPointer(0)); + } + return nullptr; +} + +//------------------------------------------------------------------------------ +vtkUnsignedCharArray* vtkWebApplication::StillRenderToBuffer( + vtkRenderWindow* view, vtkMTimeType time, int quality) +{ + vtkUnsignedCharArray* array = this->StillRender(view, quality); + if (array && array->GetMTime() != time) + { + this->LastStillRenderToMTime = array->GetMTime(); + return array; + } + return nullptr; +} + +//------------------------------------------------------------------------------ +bool vtkWebApplication::HandleInteractionEvent(vtkRenderWindow* view, vtkWebInteractionEvent* event) +{ + vtkRenderWindowInteractor* iren = nullptr; + + if (view) + { + iren = view->GetInteractor(); + } + else + { + vtkErrorMacro("Interaction not supported for view : " << view); + return false; + } + + int ctrlKey = (event->GetModifiers() & vtkWebInteractionEvent::CTRL_KEY) != 0 ? 1 : 0; + int shiftKey = (event->GetModifiers() & vtkWebInteractionEvent::SHIFT_KEY) != 0 ? 1 : 0; + + // Handle scroll action if any + if (event->GetScroll()) + { + iren->SetEventInformation(0, 0, ctrlKey, shiftKey, event->GetKeyCode(), 0); + iren->MouseMoveEvent(); + iren->RightButtonPressEvent(); + iren->SetEventInformation( + 0, event->GetScroll() * 10, ctrlKey, shiftKey, event->GetKeyCode(), 0); + iren->MouseMoveEvent(); + iren->RightButtonReleaseEvent(); + this->Internals->ImageCache[view].NeedsRender = true; + return true; + } + + const int* viewSize = view->GetSize(); + int posX = std::floor(viewSize[0] * event->GetX() + 0.5); + int posY = std::floor(viewSize[1] * event->GetY() + 0.5); + + iren->SetEventInformation( + posX, posY, ctrlKey, shiftKey, event->GetKeyCode(), event->GetRepeatCount()); + + unsigned int prev_buttons = this->Internals->ButtonStates[view]; + unsigned int changed_buttons = (event->GetButtons() ^ prev_buttons); + iren->MouseMoveEvent(); + if ((changed_buttons & vtkWebInteractionEvent::LEFT_BUTTON) != 0) + { + if ((event->GetButtons() & vtkWebInteractionEvent::LEFT_BUTTON) != 0) + { + iren->LeftButtonPressEvent(); + if (event->GetRepeatCount() > 0) + { + iren->LeftButtonReleaseEvent(); + } + } + else + { + iren->LeftButtonReleaseEvent(); + } + } + + if ((changed_buttons & vtkWebInteractionEvent::RIGHT_BUTTON) != 0) + { + if ((event->GetButtons() & vtkWebInteractionEvent::RIGHT_BUTTON) != 0) + { + iren->RightButtonPressEvent(); + if (event->GetRepeatCount() > 0) + { + iren->RightButtonPressEvent(); + } + } + else + { + iren->RightButtonReleaseEvent(); + } + } + if ((changed_buttons & vtkWebInteractionEvent::MIDDLE_BUTTON) != 0) + { + if ((event->GetButtons() & vtkWebInteractionEvent::MIDDLE_BUTTON) != 0) + { + iren->MiddleButtonPressEvent(); + if (event->GetRepeatCount() > 0) + { + iren->MiddleButtonPressEvent(); + } + } + else + { + iren->MiddleButtonReleaseEvent(); + } + } + + this->Internals->ButtonStates[view] = event->GetButtons(); + + bool needs_render = (changed_buttons != 0 || event->GetButtons()); + this->Internals->ImageCache[view].NeedsRender = needs_render; + return needs_render; +} + +//------------------------------------------------------------------------------ +const char* vtkWebApplication::GetWebGLSceneMetaData(vtkRenderWindow* view) +{ + if (!view) + { + vtkErrorMacro("No view specified."); + return nullptr; + } + + // We use the camera focal point to be the center of rotation + double centerOfRotation[3]; + vtkCamera* cam = view->GetRenderers()->GetFirstRenderer()->GetActiveCamera(); + cam->GetFocalPoint(centerOfRotation); + + if (this->Internals->ViewWebGLMap.find(view) == this->Internals->ViewWebGLMap.end()) + { + this->Internals->ViewWebGLMap[view] = vtkSmartPointer::New(); + } + + std::stringstream globalIdAsString; + globalIdAsString << this->Internals->ObjectIdMap->GetGlobalId(view); + + vtkWebGLExporter* webglExporter = this->Internals->ViewWebGLMap[view]; + webglExporter->parseScene(view->GetRenderers(), globalIdAsString.str().c_str(), VTK_PARSEALL); + + vtkInternals::WebGLObjId2IndexMap webglMap; + for (int i = 0; i < webglExporter->GetNumberOfObjects(); ++i) + { + vtkWebGLObject* wObj = webglExporter->GetWebGLObject(i); + if (wObj && wObj->isVisible()) + { + vtkInternals::WebGLObjCacheValue val; + val.ObjIndex = i; + for (int j = 0; j < wObj->GetNumberOfParts(); ++j) + { + val.BinaryParts[j] = ""; + } + webglMap[wObj->GetId()] = val; + } + } + this->Internals->WebGLExporterObjIdMap[webglExporter] = webglMap; + webglExporter->SetCenterOfRotation(static_cast(centerOfRotation[0]), + static_cast(centerOfRotation[1]), static_cast(centerOfRotation[2])); + return webglExporter->GenerateMetadata(); +} + +//------------------------------------------------------------------------------ +const char* vtkWebApplication::GetWebGLBinaryData(vtkRenderWindow* view, const char* id, int part) +{ + if (!view) + { + vtkErrorMacro("No view specified."); + return nullptr; + } + if (this->Internals->ViewWebGLMap.find(view) == this->Internals->ViewWebGLMap.end()) + { + if (this->GetWebGLSceneMetaData(view) == nullptr) + { + vtkErrorMacro("Failed to generate WebGL MetaData for: " << view); + return nullptr; + } + } + + vtkWebGLExporter* webglExporter = this->Internals->ViewWebGLMap[view]; + if (webglExporter == nullptr) + { + vtkErrorMacro("There is no cached WebGL Exporter for: " << view); + return nullptr; + } + + if (!this->Internals->WebGLExporterObjIdMap[webglExporter].empty() && + this->Internals->WebGLExporterObjIdMap[webglExporter].find(id) != + this->Internals->WebGLExporterObjIdMap[webglExporter].end()) + { + vtkInternals::WebGLObjCacheValue* cachedVal = + &(this->Internals->WebGLExporterObjIdMap[webglExporter][id]); + if (cachedVal->BinaryParts.find(part) != cachedVal->BinaryParts.end()) + { + if (cachedVal->BinaryParts[part].empty()) + { + vtkWebGLObject* obj = webglExporter->GetWebGLObject(cachedVal->ObjIndex); + if (obj && obj->isVisible()) + { + // Manage Base64 + vtkNew base64; + unsigned char* output = new unsigned char[obj->GetBinarySize(part) * 2]; + int size = + base64->Encode(obj->GetBinaryData(part), obj->GetBinarySize(part), output, false); + cachedVal->BinaryParts[part] = std::string((const char*)output, size); + delete[] output; + } + } + return cachedVal->BinaryParts[part].c_str(); + } + } + + return nullptr; +} + +//------------------------------------------------------------------------------ +void vtkWebApplication::PrintSelf(ostream& os, vtkIndent indent) +{ + this->Superclass::PrintSelf(os, indent); + os << indent << "ImageEncoding: " << this->ImageEncoding << endl; + os << indent << "ImageCompression: " << this->ImageCompression << endl; +} + +//------------------------------------------------------------------------------ +vtkObjectIdMap* vtkWebApplication::GetObjectIdMap() +{ + return this->Internals->ObjectIdMap; +} + +//------------------------------------------------------------------------------ +std::string vtkWebApplication::GetObjectId(vtkObject* obj) +{ + std::ostringstream oss; + oss << std::hex << static_cast(obj); + return oss.str(); +} +VTK_ABI_NAMESPACE_END diff --git a/Web/Core/vtkWebApplication.h b/Web/Core/vtkWebApplication.h new file mode 100644 index 000000000..80b9a10b9 --- /dev/null +++ b/Web/Core/vtkWebApplication.h @@ -0,0 +1,144 @@ +// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen +// SPDX-License-Identifier: BSD-3-Clause +/** + * @class vtkWebApplication + * @brief defines ParaViewWeb application interface. + * + * vtkWebApplication defines the core interface for a ParaViewWeb application. + * This exposes methods that make it easier to manage views and rendered images + * from views. + */ + +#ifndef vtkWebApplication_h +#define vtkWebApplication_h + +#include "vtkObject.h" +#include "vtkWebCoreModule.h" // needed for exports +#include // needed for std::string + +VTK_ABI_NAMESPACE_BEGIN +class vtkObjectIdMap; +class vtkRenderWindow; +class vtkUnsignedCharArray; +class vtkWebInteractionEvent; + +class VTKWEBCORE_EXPORT vtkWebApplication : public vtkObject +{ +public: + static vtkWebApplication* New(); + vtkTypeMacro(vtkWebApplication, vtkObject); + void PrintSelf(ostream& os, vtkIndent indent) override; + + ///@{ + /** + * Set the encoding to be used for rendered images. + */ + enum + { + ENCODING_NONE = 0, + ENCODING_BASE64 = 1 + }; + vtkSetClampMacro(ImageEncoding, int, ENCODING_NONE, ENCODING_BASE64); + vtkGetMacro(ImageEncoding, int); + ///@} + + ///@{ + /** + * Set the compression to be used for rendered images. + */ + enum + { + COMPRESSION_NONE = 0, + COMPRESSION_PNG = 1, + COMPRESSION_JPEG = 2 + }; + vtkSetClampMacro(ImageCompression, int, COMPRESSION_NONE, COMPRESSION_JPEG); + vtkGetMacro(ImageCompression, int); + ///@} + + ///@{ + /** + * Set the number of worker threads to use for image encoding. Calling this + * method with a number greater than 32 or less than zero will have no effect. + */ + void SetNumberOfEncoderThreads(vtkTypeUInt32); + vtkTypeUInt32 GetNumberOfEncoderThreads(); + ///@} + + ///@{ + /** + * Render a view and obtain the rendered image. + */ + vtkUnsignedCharArray* StillRender(vtkRenderWindow* view, int quality = 100); + vtkUnsignedCharArray* InteractiveRender(vtkRenderWindow* view, int quality = 50); + const char* StillRenderToString(vtkRenderWindow* view, vtkMTimeType time = 0, int quality = 100); + vtkUnsignedCharArray* StillRenderToBuffer( + vtkRenderWindow* view, vtkMTimeType time = 0, int quality = 100); + ///@} + + /** + * StillRenderToString() need not necessary returns the most recently rendered + * image. Use this method to get whether there are any pending images being + * processed concurrently. + */ + bool GetHasImagesBeingProcessed(vtkRenderWindow*); + + /** + * Communicate mouse interaction to a view. + * Returns true if the interaction changed the view state, otherwise returns false. + */ + bool HandleInteractionEvent(vtkRenderWindow* view, vtkWebInteractionEvent* event); + + /** + * Invalidate view cache + */ + void InvalidateCache(vtkRenderWindow* view); + + ///@{ + /** + * Return the MTime of the last array exported by StillRenderToString. + */ + vtkGetMacro(LastStillRenderToMTime, vtkMTimeType); + ///@} + + /** + * Return the Meta data description of the input scene in JSON format. + * This is using the vtkWebGLExporter to parse the scene. + * NOTE: This should be called before getting the webGL binary data. + */ + const char* GetWebGLSceneMetaData(vtkRenderWindow* view); + + /** + * Return the binary data given the part index + * and the webGL object piece id in the scene. + */ + const char* GetWebGLBinaryData(vtkRenderWindow* view, const char* id, int partIndex); + + vtkObjectIdMap* GetObjectIdMap(); + + /** + * Return a hexadecimal formatted string of the VTK object's memory address, + * useful for uniquely identifying the object when exporting data. + * + * e.g. 0x8f05a90 + */ + static std::string GetObjectId(vtkObject* obj); + +protected: + vtkWebApplication(); + ~vtkWebApplication() override; + + int ImageEncoding; + int ImageCompression; + vtkMTimeType LastStillRenderToMTime; + +private: + vtkWebApplication(const vtkWebApplication&) = delete; + void operator=(const vtkWebApplication&) = delete; + + class vtkInternals; + vtkInternals* Internals; +}; + +VTK_ABI_NAMESPACE_END +#endif diff --git a/Web/Core/vtkWebInteractionEvent.cxx b/Web/Core/vtkWebInteractionEvent.cxx new file mode 100644 index 000000000..bc988fee6 --- /dev/null +++ b/Web/Core/vtkWebInteractionEvent.cxx @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen +// SPDX-License-Identifier: BSD-3-Clause +#include "vtkWebInteractionEvent.h" + +#include "vtkObjectFactory.h" + +VTK_ABI_NAMESPACE_BEGIN +vtkStandardNewMacro(vtkWebInteractionEvent); +//------------------------------------------------------------------------------ +vtkWebInteractionEvent::vtkWebInteractionEvent() + : Buttons(0) + , Modifiers(0) + , KeyCode(0) + , X(0.0) + , Y(0.0) + , Scroll(0.0) + , RepeatCount(0) +{ +} + +//------------------------------------------------------------------------------ +vtkWebInteractionEvent::~vtkWebInteractionEvent() = default; + +//------------------------------------------------------------------------------ +void vtkWebInteractionEvent::PrintSelf(ostream& os, vtkIndent indent) +{ + this->Superclass::PrintSelf(os, indent); + os << indent << "Buttons: " << this->Buttons << endl; + os << indent << "Modifiers: " << this->Modifiers << endl; + os << indent << "KeyCode: " << static_cast(this->KeyCode) << endl; + os << indent << "X: " << this->X << endl; + os << indent << "Y: " << this->Y << endl; + os << indent << "RepeatCount: " << this->RepeatCount << endl; + os << indent << "Scroll: " << this->Scroll << endl; +} +VTK_ABI_NAMESPACE_END diff --git a/Web/Core/vtkWebInteractionEvent.h b/Web/Core/vtkWebInteractionEvent.h new file mode 100644 index 000000000..e088ccf25 --- /dev/null +++ b/Web/Core/vtkWebInteractionEvent.h @@ -0,0 +1,96 @@ +// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen +// SPDX-License-Identifier: BSD-3-Clause +/** + * @class vtkWebInteractionEvent + * + * + */ + +#ifndef vtkWebInteractionEvent_h +#define vtkWebInteractionEvent_h + +#include "vtkObject.h" +#include "vtkWebCoreModule.h" // needed for exports + +VTK_ABI_NAMESPACE_BEGIN +class VTKWEBCORE_EXPORT vtkWebInteractionEvent : public vtkObject +{ +public: + static vtkWebInteractionEvent* New(); + vtkTypeMacro(vtkWebInteractionEvent, vtkObject); + void PrintSelf(ostream& os, vtkIndent indent) override; + + enum MouseButton + { + LEFT_BUTTON = 0x01, + MIDDLE_BUTTON = 0x02, + RIGHT_BUTTON = 0x04 + }; + + enum ModifierKeys + { + SHIFT_KEY = 0x01, + CTRL_KEY = 0x02, + ALT_KEY = 0x04, + META_KEY = 0x08 + }; + + ///@{ + /** + * Set/Get the mouse buttons state. + */ + vtkSetMacro(Buttons, unsigned int); + vtkGetMacro(Buttons, unsigned int); + ///@} + + ///@{ + /** + * Set/Get modifier state. + */ + vtkSetMacro(Modifiers, unsigned int); + vtkGetMacro(Modifiers, unsigned int); + ///@} + + ///@{ + /** + * Set/Get the chart code. + */ + vtkSetMacro(KeyCode, char); + vtkGetMacro(KeyCode, char); + ///@} + + ///@{ + /** + * Set/Get event position. + */ + vtkSetMacro(X, double); + vtkGetMacro(X, double); + vtkSetMacro(Y, double); + vtkGetMacro(Y, double); + vtkSetMacro(Scroll, double); + vtkGetMacro(Scroll, double); + ///@} + + // Handle double click + vtkSetMacro(RepeatCount, int); + vtkGetMacro(RepeatCount, int); + +protected: + vtkWebInteractionEvent(); + ~vtkWebInteractionEvent() override; + + unsigned int Buttons; + unsigned int Modifiers; + char KeyCode; + double X; + double Y; + double Scroll; + int RepeatCount; + +private: + vtkWebInteractionEvent(const vtkWebInteractionEvent&) = delete; + void operator=(const vtkWebInteractionEvent&) = delete; +}; + +VTK_ABI_NAMESPACE_END +#endif diff --git a/Web/Core/vtkWebUtilities.cxx b/Web/Core/vtkWebUtilities.cxx new file mode 100644 index 000000000..b06c31a96 --- /dev/null +++ b/Web/Core/vtkWebUtilities.cxx @@ -0,0 +1,118 @@ +// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen +// SPDX-License-Identifier: BSD-3-Clause +#include "vtkWebUtilities.h" +#include "vtkPython.h" // Need to be first and used for Py_xxx macros + +#include "vtkDataSet.h" +#include "vtkDataSetAttributes.h" +#include "vtkJavaScriptDataWriter.h" +#include "vtkMultiProcessController.h" +#include "vtkNew.h" +#include "vtkObjectFactory.h" +#include "vtkSplitColumnComponents.h" +#include "vtkTable.h" + +#include + +VTK_ABI_NAMESPACE_BEGIN +vtkStandardNewMacro(vtkWebUtilities); +//------------------------------------------------------------------------------ +vtkWebUtilities::vtkWebUtilities() = default; + +//------------------------------------------------------------------------------ +vtkWebUtilities::~vtkWebUtilities() = default; + +//------------------------------------------------------------------------------ +std::string vtkWebUtilities::WriteAttributesToJavaScript(int field_type, vtkDataSet* dataset) +{ + if (dataset == nullptr || + (field_type != vtkDataObject::POINT && field_type != vtkDataObject::CELL)) + { + return "[]"; + } + + std::ostringstream stream; + + vtkNew clone; + clone->PassData(dataset->GetAttributes(field_type)); + clone->RemoveArray("vtkValidPointMask"); + + vtkNew table; + table->SetRowData(clone); + + vtkNew splitter; + splitter->SetInputDataObject(table); + splitter->Update(); + + vtkNew writer; + writer->SetOutputStream(&stream); + writer->SetInputDataObject(splitter->GetOutputDataObject(0)); + writer->SetVariableName(nullptr); + writer->SetIncludeFieldNames(false); + writer->Write(); + + return stream.str(); +} + +//------------------------------------------------------------------------------ +std::string vtkWebUtilities::WriteAttributeHeadersToJavaScript(int field_type, vtkDataSet* dataset) +{ + if (dataset == nullptr || + (field_type != vtkDataObject::POINT && field_type != vtkDataObject::CELL)) + { + return "[]"; + } + + std::ostringstream stream; + stream << "["; + + vtkDataSetAttributes* dsa = dataset->GetAttributes(field_type); + vtkNew clone; + clone->CopyAllocate(dsa, 0); + clone->RemoveArray("vtkValidPointMask"); + + vtkNew table; + table->SetRowData(clone); + + vtkNew splitter; + splitter->SetInputDataObject(table); + splitter->Update(); + + dsa = vtkTable::SafeDownCast(splitter->GetOutputDataObject(0))->GetRowData(); + + for (int cc = 0; cc < dsa->GetNumberOfArrays(); cc++) + { + const char* name = dsa->GetArrayName(cc); + if (cc != 0) + { + stream << ", "; + } + stream << "\"" << (name ? name : "") << "\""; + } + stream << "]"; + return stream.str(); +} + +//------------------------------------------------------------------------------ +void vtkWebUtilities::PrintSelf(ostream& os, vtkIndent indent) +{ + this->Superclass::PrintSelf(os, indent); +} + +//------------------------------------------------------------------------------ +void vtkWebUtilities::ProcessRMIs() +{ + vtkWebUtilities::ProcessRMIs(1, 0); +} + +//------------------------------------------------------------------------------ +void vtkWebUtilities::ProcessRMIs(int reportError, int dont_loop) +{ + Py_BEGIN_ALLOW_THREADS + + vtkMultiProcessController::GetGlobalController() + ->ProcessRMIs(reportError, dont_loop); + + Py_END_ALLOW_THREADS +} +VTK_ABI_NAMESPACE_END diff --git a/Web/Core/vtkWebUtilities.h b/Web/Core/vtkWebUtilities.h new file mode 100644 index 000000000..2b76ef9cd --- /dev/null +++ b/Web/Core/vtkWebUtilities.h @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen +// SPDX-License-Identifier: BSD-3-Clause +/** + * @class vtkWebUtilities + * @brief collection of utility functions for ParaView Web. + * + * vtkWebUtilities consolidates miscellaneous utility functions useful for + * Python scripts designed for ParaView Web. + */ + +#ifndef vtkWebUtilities_h +#define vtkWebUtilities_h + +#include "vtkObject.h" +#include "vtkWebCoreModule.h" // needed for exports +#include // for std::string + +VTK_ABI_NAMESPACE_BEGIN +class vtkDataSet; + +class VTKWEBCORE_EXPORT vtkWebUtilities : public vtkObject +{ +public: + static vtkWebUtilities* New(); + vtkTypeMacro(vtkWebUtilities, vtkObject); + void PrintSelf(ostream& os, vtkIndent indent) override; + + static std::string WriteAttributesToJavaScript(int field_type, vtkDataSet*); + static std::string WriteAttributeHeadersToJavaScript(int field_type, vtkDataSet*); + + ///@{ + /** + * This method is similar to the ProcessRMIs() method on the GlobalController + * except that it is Python friendly in the sense that it will release the + * Python GIS lock, so when run in a thread, this will truly work in the + * background without locking the main one. + */ + static void ProcessRMIs(); + static void ProcessRMIs(int reportError, int dont_loop = 0); + ///@} + +protected: + vtkWebUtilities(); + ~vtkWebUtilities() override; + +private: + vtkWebUtilities(const vtkWebUtilities&) = delete; + void operator=(const vtkWebUtilities&) = delete; +}; + +VTK_ABI_NAMESPACE_END +#endif diff --git a/Web/Python/CMakeLists.txt b/Web/Python/CMakeLists.txt new file mode 100644 index 000000000..5254e772d --- /dev/null +++ b/Web/Python/CMakeLists.txt @@ -0,0 +1,25 @@ +set(files + vtkmodules/web/__init__.py + vtkmodules/web/camera.py + vtkmodules/web/dataset_builder.py + vtkmodules/web/errors.py + vtkmodules/web/protocols.py + vtkmodules/web/query_data_model.py + vtkmodules/web/render_window_serializer.py + vtkmodules/web/testing.py + vtkmodules/web/vtkjs_helper.py + vtkmodules/web/venv.py + vtkmodules/web/wslink.py + vtkmodules/web/utils.py) + +vtk_module_add_python_package(VTK::WebPython + FILES ${files} + PACKAGE "vtkmodules.web" + MODULE_DESTINATION "${VTK_PYTHON_SITE_PACKAGES_SUFFIX}") + +vtk_module_add_python_module(VTK::WebPython + PACKAGES "vtkmodules.web") + +set_property(GLOBAL APPEND + PROPERTY + vtk_web_python_modules "wslink>=1.0.4") diff --git a/Web/Python/Testing/CMakeLists.txt b/Web/Python/Testing/CMakeLists.txt new file mode 100644 index 000000000..e33473e93 --- /dev/null +++ b/Web/Python/Testing/CMakeLists.txt @@ -0,0 +1,3 @@ +if (VTK_WRAP_PYTHON) + add_subdirectory(Python) +endif () diff --git a/Web/Python/Testing/Python/CMakeLists.txt b/Web/Python/Testing/Python/CMakeLists.txt new file mode 100644 index 000000000..44d6482b5 --- /dev/null +++ b/Web/Python/Testing/Python/CMakeLists.txt @@ -0,0 +1,4 @@ +vtk_add_test_python( + NO_DATA NO_VALID NO_OUTPUT + TestSerializeRenderWindow.py + ) diff --git a/Web/Python/Testing/Python/TestSerializeRenderWindow.py b/Web/Python/Testing/Python/TestSerializeRenderWindow.py new file mode 100644 index 000000000..933675a4f --- /dev/null +++ b/Web/Python/Testing/Python/TestSerializeRenderWindow.py @@ -0,0 +1,43 @@ +import json +from vtkmodules.vtkFiltersSources import vtkConeSource +from vtkmodules.vtkRenderingCore import ( + vtkActor, + vtkPolyDataMapper, + vtkRenderWindow, + vtkRenderer, +) +import vtkmodules.vtkRenderingFreeType +import vtkmodules.vtkRenderingOpenGL2 +from vtkmodules.web import render_window_serializer as rws +from vtkmodules.test import Testing + +class TestSerializeRenderWindow(Testing.vtkTest): + def testSerializeRenderWindow(self): + cone = vtkConeSource() + + coneMapper = vtkPolyDataMapper() + coneMapper.SetInputConnection(cone.GetOutputPort()) + + coneActor = vtkActor() + coneActor.SetMapper(coneMapper) + + ren = vtkRenderer() + ren.AddActor(coneActor) + renWin = vtkRenderWindow() + renWin.AddRenderer(ren) + + ren.ResetCamera() + renWin.Render() + + # Exercise some of the serialization functionality and make sure it + # does not generate a stack trace + context = rws.SynchronizationContext() + rws.initializeSerializers() + jsonData = rws.serializeInstance(None, renWin, rws.getReferenceId(renWin), context, 0) + + # jsonStr = json.dumps(jsonData) + # print jsonStr + # print len(jsonStr) + +if __name__ == "__main__": + Testing.main([(TestSerializeRenderWindow, 'test')]) diff --git a/Web/Python/vtk.module b/Web/Python/vtk.module new file mode 100644 index 000000000..f5b04d507 --- /dev/null +++ b/Web/Python/vtk.module @@ -0,0 +1,22 @@ +NAME + VTK::WebPython +LIBRARY_NAME + vtkWebPython +CONDITION + VTK_WRAP_PYTHON +GROUPS + Web +SPDX_LICENSE_IDENTIFIER + BSD-3-Clause +SPDX_COPYRIGHT_TEXT + Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen +DEPENDS + VTK::CommonCore +PRIVATE_DEPENDS + VTK::FiltersGeometry + VTK::WebCore +TEST_DEPENDS + VTK::TestingCore +TEST_LABELS + VTK::Web +EXCLUDE_WRAP diff --git a/Web/Python/vtkmodules/web/__init__.py b/Web/Python/vtkmodules/web/__init__.py new file mode 100644 index 000000000..60e9ae878 --- /dev/null +++ b/Web/Python/vtkmodules/web/__init__.py @@ -0,0 +1,62 @@ +import hashlib, base64 + +arrayTypesMapping = [ + " ", # VTK_VOID 0 + " ", # VTK_BIT 1 + "b", # VTK_CHAR 2 + "B", # VTK_UNSIGNED_CHAR 3 + "h", # VTK_SHORT 4 + "H", # VTK_UNSIGNED_SHORT 5 + "i", # VTK_INT 6 + "I", # VTK_UNSIGNED_INT 7 + "l", # VTK_LONG 8 + "L", # VTK_UNSIGNED_LONG 9 + "f", # VTK_FLOAT 10 + "d", # VTK_DOUBLE 11 + "L", # VTK_ID_TYPE 12 + " ", # unspecified 13 + " ", # unspecified 14 + "b", # signed_char 15 +] + +javascriptMapping = { + "b": "Int8Array", + "B": "Uint8Array", + "h": "Int16Array", + "H": "Int16Array", + "i": "Int32Array", + "I": "Uint32Array", + "l": "Int32Array", + "L": "Uint32Array", + "f": "Float32Array", + "d": "Float64Array", +} + + +def iteritems(d, **kwargs): + return iter(d.items(**kwargs)) + + +def base64Encode(x): + return base64.b64encode(x).decode("utf-8") + + +def hashDataArray(dataArray): + hashedBit = hashlib.md5(memoryview(dataArray)).hexdigest() + typeCode = arrayTypesMapping[dataArray.GetDataType()] + return "%s_%d%s" % (hashedBit, dataArray.GetSize(), typeCode) + + +def getJSArrayType(dataArray): + return javascriptMapping[arrayTypesMapping[dataArray.GetDataType()]] + + +def getReferenceId(ref): + if ref: + try: + return ref.__this__[1:17] + except: + idStr = str(ref)[-12:-1] + # print('====> fallback ID %s for %s' % (idStr, ref)) + return idStr + return "0x0" diff --git a/Web/Python/vtkmodules/web/camera.py b/Web/Python/vtkmodules/web/camera.py new file mode 100644 index 000000000..bd706b114 --- /dev/null +++ b/Web/Python/vtkmodules/web/camera.py @@ -0,0 +1,640 @@ +from math import * + +# ----------------------------------------------------------------------------- +# Set of helper functions +# ----------------------------------------------------------------------------- + + +def normalize(vect, tolerance=0.00001): + mag2 = sum(n * n for n in vect) + if abs(mag2 - 1.0) > tolerance: + mag = sqrt(mag2) + vect = tuple(n / mag for n in vect) + return vect + + +def q_mult(q1, q2): + w1, x1, y1, z1 = q1 + w2, x2, y2, z2 = q2 + w = w1 * w2 - x1 * x2 - y1 * y2 - z1 * z2 + x = w1 * x2 + x1 * w2 + y1 * z2 - z1 * y2 + y = w1 * y2 + y1 * w2 + z1 * x2 - x1 * z2 + z = w1 * z2 + z1 * w2 + x1 * y2 - y1 * x2 + return w, x, y, z + + +def q_conjugate(q): + w, x, y, z = q + return (w, -x, -y, -z) + + +def qv_mult(q1, v1): + q2 = (0.0,) + v1 + return q_mult(q_mult(q1, q2), q_conjugate(q1))[1:] + + +def axisangle_to_q(v, theta): + v = normalize(v) + x, y, z = v + theta /= 2 + w = cos(theta) + x = x * sin(theta) + y = y * sin(theta) + z = z * sin(theta) + return w, x, y, z + + +def vectProduct(axisA, axisB): + xa, ya, za = axisA + xb, yb, zb = axisB + normalVect = (ya * zb - za * yb, za * xb - xa * zb, xa * yb - ya * xb) + normalVect = normalize(normalVect) + return normalVect + + +def dotProduct(vecA, vecB): + return (vecA[0] * vecB[0]) + (vecA[1] * vecB[1]) + (vecA[2] * vecB[2]) + + +def rotate(axis, angle, center, point): + angleInRad = 3.141592654 * angle / 180.0 + rotation = axisangle_to_q(axis, angleInRad) + tPoint = tuple((point[i] - center[i]) for i in range(3)) + rtPoint = qv_mult(rotation, tPoint) + rPoint = tuple((rtPoint[i] + center[i]) for i in range(3)) + return rPoint + + +# ----------------------------------------------------------------------------- +# Spherical Camera +# ----------------------------------------------------------------------------- + + +class SphericalCamera(object): + def __init__( + self, dataHandler, focalPoint, position, phiAxis, phiAngles, thetaAngles + ): + self.dataHandler = dataHandler + self.cameraSettings = [] + self.thetaBind = { + "mouse": { + "drag": {"modifier": 0, "coordinate": 1, "step": 30, "orientation": 1} + } + } + self.phiBind = { + "mouse": { + "drag": {"modifier": 0, "coordinate": 0, "step": 30, "orientation": 1} + } + } + + # Convert to serializable type + fp = tuple(i for i in focalPoint) + + # Register arguments to the data handler + if len(phiAngles) > 1 and phiAngles[-1] + phiAngles[1] == 360: + self.dataHandler.registerArgument( + priority=0, + name="phi", + values=phiAngles, + ui="slider", + loop="modulo", + bind=self.phiBind, + ) + else: + self.dataHandler.registerArgument( + priority=0, name="phi", values=phiAngles, ui="slider", bind=self.phiBind + ) + if thetaAngles[0] < 0 and thetaAngles[0] >= -90: + idx = 0 + for theta in thetaAngles: + if theta < 0: + idx += 1 + + self.dataHandler.registerArgument( + priority=0, + name="theta", + values=[(x + 90) for x in thetaAngles], + ui="slider", + default=idx, + bind=self.thetaBind, + ) + else: + self.dataHandler.registerArgument( + priority=0, + name="theta", + values=thetaAngles, + ui="slider", + bind=self.thetaBind, + ) + + # Compute all camera settings + for theta in thetaAngles: + for phi in phiAngles: + phiPos = rotate(phiAxis, -phi, fp, position) + thetaAxis = vectProduct( + phiAxis, tuple(fp[i] - phiPos[i] for i in range(3)) + ) + thetaPhiPos = rotate(thetaAxis, theta, fp, phiPos) + viewUp = rotate(thetaAxis, theta, (0, 0, 0), phiAxis) + + self.cameraSettings.append( + { + "theta": theta, + "thetaIdx": thetaAngles.index(theta), + "phi": phi, + "phiIdx": phiAngles.index(phi), + "focalPoint": fp, + "position": thetaPhiPos, + "viewUp": viewUp, + } + ) + + self.dataHandler.updateBasePattern() + + def updatePriority(self, priorityList): + keyList = ["theta", "phi"] + for idx in range(min(len(priorityList), len(keyList))): + self.dataHandler.updatePriority(keyList[idx], priorityList[idx]) + + def __iter__(self): + for cameraData in self.cameraSettings: + self.dataHandler.setArguments( + phi=cameraData["phiIdx"], theta=cameraData["thetaIdx"] + ) + yield cameraData + + +# ----------------------------------------------------------------------------- +# Cylindrical Camera +# ----------------------------------------------------------------------------- + + +class CylindricalCamera(object): + def __init__( + self, + dataHandler, + focalPoint, + position, + rotationAxis, + phiAngles, + translationValues, + ): + self.dataHandler = dataHandler + self.cameraSettings = [] + + # Register arguments to the data handler + self.dataHandler.registerArgument( + priority=0, name="phi", values=phiAngles, ui="slider", loop="modulo" + ) + self.dataHandler.registerArgument( + priority=0, name="n_pos", values=translationValues, ui="slider" + ) + + # Compute all camera settings + for translation in translationValues: + for phi in phiAngles: + phiPos = rotate(rotationAxis, phi, focalPoint, position) + newfocalPoint = tuple( + focalPoint[i] + (translation * rotationAxis[i]) for i in range(3) + ) + transPhiPoint = tuple( + phiPos[i] + (translation * rotationAxis[i]) for i in range(3) + ) + + self.cameraSettings.append( + { + "n_pos": translation, + "n_posIdx": translationValues.index(translation), + "phi": phi, + "phiIdx": phiAngles.index(phi), + "focalPoint": newfocalPoint, + "position": transPhiPoint, + "viewUp": rotationAxis, + } + ) + + self.dataHandler.updateBasePattern() + + def updatePriority(self, priorityList): + keyList = ["n_pos", "phi"] + for idx in range(min(len(priorityList), len(keyList))): + self.dataHandler.updatePriority(keyList[idx], priorityList[idx]) + + def __iter__(self): + for cameraData in self.cameraSettings: + self.dataHandler.setArguments( + phi=cameraData["phiIdx"], n_pos=cameraData["n_posIdx"] + ) + yield cameraData + + +# ----------------------------------------------------------------------------- +# MultiView Cube Camera +# ----------------------------------------------------------------------------- + + +class CubeCamera(object): + + # positions = [ { position: [x,y,z], args: { i: 1, j: 0, k: 7 } }, ... ] + def __init__(self, dataHandler, viewForward, viewUp, positions): + self.dataHandler = dataHandler + self.cameraSettings = [] + self.viewForward = viewForward + self.viewUp = viewUp + self.rightDirection = vectProduct(viewForward, viewUp) + self.positions = positions + + # Register arguments to the data handler + self.dataHandler.registerArgument( + priority=0, name="orientation", values=["f", "b", "r", "l", "u", "d"] + ) + + # Register arguments to id position + self.args = {} + for pos in positions: + for key in pos["args"]: + if key not in self.args: + self.args[key] = {} + self.args[key][pos["args"][key]] = True + + for key in self.args: + self.args[key] = sorted(self.args[key], key=lambda k: int(k)) + + self.keyList = self.args.keys() + for key in self.args: + self.dataHandler.registerArgument( + priority=1, name=key, values=self.args[key] + ) + + self.dataHandler.updateBasePattern() + + def updatePriority(self, priorityList): + keyList = ["orientation"] + for idx in range(min(len(priorityList), len(keyList))): + self.dataHandler.updatePriority(keyList[idx], priorityList[idx]) + + def __iter__(self): + for pos in self.positions: + cameraData = { + "position": pos["position"], + } + + print("=" * 80) + for key in pos["args"]: + idx = self.args[key].index(pos["args"][key]) + self.dataHandler.setArguments(**{key: idx}) + print(key, idx) + + print("position", cameraData["position"]) + + # front + cameraData["focalPoint"] = [ + (cameraData["position"][i] + self.viewForward[i]) for i in range(3) + ] + cameraData["viewUp"] = [self.viewUp[i] for i in range(3)] + cameraData["orientation"] = "front" + self.dataHandler.setArguments(orientation=0) + yield cameraData + + # back + cameraData["focalPoint"] = [ + (cameraData["position"][i] - self.viewForward[i]) for i in range(3) + ] + cameraData["viewUp"] = [self.viewUp[i] for i in range(3)] + cameraData["orientation"] = "back" + self.dataHandler.setArguments(orientation=1) + yield cameraData + + # right + self.dataHandler.setArguments(orientation=2) + cameraData["focalPoint"] = [ + (cameraData["position"][i] + self.rightDirection[i]) for i in range(3) + ] + cameraData["viewUp"] = [self.viewUp[i] for i in range(3)] + cameraData["orientation"] = "right" + yield cameraData + + # left + self.dataHandler.setArguments(orientation=3) + cameraData["focalPoint"] = [ + (cameraData["position"][i] - self.rightDirection[i]) for i in range(3) + ] + cameraData["viewUp"] = [self.viewUp[i] for i in range(3)] + cameraData["orientation"] = "left" + yield cameraData + + # up + self.dataHandler.setArguments(orientation=4) + cameraData["focalPoint"] = [ + (cameraData["position"][i] + self.viewUp[i]) for i in range(3) + ] + cameraData["viewUp"] = [(-self.viewForward[i]) for i in range(3)] + cameraData["orientation"] = "up" + yield cameraData + + # doww + self.dataHandler.setArguments(orientation=5) + cameraData["focalPoint"] = [ + (cameraData["position"][i] - self.viewUp[i]) for i in range(3) + ] + cameraData["viewUp"] = [self.viewForward[i] for i in range(3)] + cameraData["orientation"] = "down" + yield cameraData + + +# ----------------------------------------------------------------------------- +# MultiView Cube Camera +# ----------------------------------------------------------------------------- + + +class StereoCubeCamera(object): + + # positions = [ { position: [x,y,z], args: { i: 1, j: 0, k: 7 } }, ... ] + def __init__(self, dataHandler, viewForward, viewUp, positions, eyeSpacing): + self.dataHandler = dataHandler + self.cameraSettings = [] + self.viewForward = viewForward + self.viewUp = viewUp + self.rightDirection = vectProduct(viewForward, viewUp) + self.positions = positions + self.eyeSpacing = eyeSpacing + + # Register arguments to the data handler + self.dataHandler.registerArgument( + priority=0, name="orientation", values=["f", "b", "r", "l", "u", "d"] + ) + self.dataHandler.registerArgument( + priority=0, name="eye", values=["left", "right"] + ) + + # Register arguments to id position + self.args = {} + for pos in positions: + for key in pos["args"]: + if key not in self.args: + self.args[key] = {} + self.args[key][pos["args"][key]] = True + + for key in self.args: + self.args[key] = sorted(self.args[key], key=lambda k: int(k)) + + self.keyList = self.args.keys() + for key in self.args: + self.dataHandler.registerArgument( + priority=1, name=key, values=self.args[key] + ) + + self.dataHandler.updateBasePattern() + + def updatePriority(self, priorityList): + keyList = ["orientation"] + for idx in range(min(len(priorityList), len(keyList))): + self.dataHandler.updatePriority(keyList[idx], priorityList[idx]) + + def __iter__(self): + for pos in self.positions: + cameraData = {} + + for key in pos["args"]: + idx = self.args[key].index(pos["args"][key]) + self.dataHandler.setArguments(**{key: idx}) + + # front + cameraData["orientation"] = "front" + self.dataHandler.setArguments(orientation=0) + deltaVect = [ + (v * float(self.eyeSpacing) * 0.5) for v in self.rightDirection + ] + ## Left-Eye + self.dataHandler.setArguments(eye=0) + cameraData["viewUp"] = [self.viewUp[i] for i in range(3)] + cameraData["position"] = [ + (pos["position"][idx] - deltaVect[idx]) for idx in range(3) + ] + cameraData["focalPoint"] = [ + (pos["position"][i] + self.viewForward[i] - deltaVect[i]) + for i in range(3) + ] + yield cameraData + ## Right-Eye + self.dataHandler.setArguments(eye=1) + cameraData["viewUp"] = [self.viewUp[i] for i in range(3)] + cameraData["position"] = [ + (pos["position"][idx] + deltaVect[idx]) for idx in range(3) + ] + cameraData["focalPoint"] = [ + (pos["position"][i] + self.viewForward[i] + deltaVect[i]) + for i in range(3) + ] + yield cameraData + + # back + cameraData["orientation"] = "back" + self.dataHandler.setArguments(orientation=1) + deltaVect = [ + -(v * float(self.eyeSpacing) * 0.5) for v in self.rightDirection + ] + ## Left-Eye + self.dataHandler.setArguments(eye=0) + cameraData["viewUp"] = [self.viewUp[i] for i in range(3)] + cameraData["position"] = [ + (pos["position"][idx] - deltaVect[idx]) for idx in range(3) + ] + cameraData["focalPoint"] = [ + (pos["position"][i] - self.viewForward[i] - deltaVect[i]) + for i in range(3) + ] + yield cameraData + ## Right-Eye + self.dataHandler.setArguments(eye=1) + cameraData["viewUp"] = [self.viewUp[i] for i in range(3)] + cameraData["position"] = [ + (pos["position"][idx] + deltaVect[idx]) for idx in range(3) + ] + cameraData["focalPoint"] = [ + (pos["position"][i] - self.viewForward[i] + deltaVect[i]) + for i in range(3) + ] + yield cameraData + + # right + self.dataHandler.setArguments(orientation=2) + cameraData["orientation"] = "right" + deltaVect = [-(v * float(self.eyeSpacing) * 0.5) for v in self.viewForward] + ## Left-Eye + self.dataHandler.setArguments(eye=0) + cameraData["viewUp"] = [self.viewUp[i] for i in range(3)] + cameraData["position"] = [ + (pos["position"][idx] - deltaVect[idx]) for idx in range(3) + ] + cameraData["focalPoint"] = [ + (pos["position"][i] + self.rightDirection[i] - deltaVect[i]) + for i in range(3) + ] + yield cameraData + ## Right-Eye + self.dataHandler.setArguments(eye=1) + cameraData["viewUp"] = [self.viewUp[i] for i in range(3)] + cameraData["position"] = [ + (pos["position"][idx] + deltaVect[idx]) for idx in range(3) + ] + cameraData["focalPoint"] = [ + (pos["position"][i] + self.rightDirection[i] + deltaVect[i]) + for i in range(3) + ] + yield cameraData + + # left + self.dataHandler.setArguments(orientation=3) + cameraData["orientation"] = "left" + deltaVect = [(v * float(self.eyeSpacing) * 0.5) for v in self.viewForward] + ## Left-Eye + self.dataHandler.setArguments(eye=0) + cameraData["viewUp"] = [self.viewUp[i] for i in range(3)] + cameraData["position"] = [ + (pos["position"][idx] - deltaVect[idx]) for idx in range(3) + ] + cameraData["focalPoint"] = [ + (pos["position"][i] - self.rightDirection[i] - deltaVect[i]) + for i in range(3) + ] + yield cameraData + ## Right-Eye + self.dataHandler.setArguments(eye=1) + cameraData["viewUp"] = [self.viewUp[i] for i in range(3)] + cameraData["position"] = [ + (pos["position"][idx] + deltaVect[idx]) for idx in range(3) + ] + cameraData["focalPoint"] = [ + (pos["position"][i] - self.rightDirection[i] + deltaVect[i]) + for i in range(3) + ] + yield cameraData + + # up + self.dataHandler.setArguments(orientation=4) + cameraData["orientation"] = "up" + deltaVect = [ + (v * float(self.eyeSpacing) * 0.5) for v in self.rightDirection + ] + ## Left-Eye + self.dataHandler.setArguments(eye=0) + cameraData["viewUp"] = [(-self.viewForward[i]) for i in range(3)] + cameraData["position"] = [ + (pos["position"][idx] - deltaVect[idx]) for idx in range(3) + ] + cameraData["focalPoint"] = [ + (pos["position"][i] + self.viewUp[i] - deltaVect[i]) for i in range(3) + ] + yield cameraData + ## Right-Eye + self.dataHandler.setArguments(eye=1) + cameraData["viewUp"] = [(-self.viewForward[i]) for i in range(3)] + cameraData["position"] = [ + (pos["position"][idx] + deltaVect[idx]) for idx in range(3) + ] + cameraData["focalPoint"] = [ + (pos["position"][i] + self.viewUp[i] + deltaVect[i]) for i in range(3) + ] + yield cameraData + + # doww + self.dataHandler.setArguments(orientation=5) + cameraData["orientation"] = "down" + deltaVect = [ + (v * float(self.eyeSpacing) * 0.5) for v in self.rightDirection + ] + ## Left-Eye + self.dataHandler.setArguments(eye=0) + cameraData["viewUp"] = [self.viewForward[i] for i in range(3)] + cameraData["position"] = [ + (pos["position"][idx] - deltaVect[idx]) for idx in range(3) + ] + cameraData["focalPoint"] = [ + (pos["position"][i] - self.viewUp[i] - deltaVect[i]) for i in range(3) + ] + yield cameraData + ## Right-Eye + self.dataHandler.setArguments(eye=1) + cameraData["viewUp"] = [self.viewForward[i] for i in range(3)] + cameraData["position"] = [ + (pos["position"][idx] + deltaVect[idx]) for idx in range(3) + ] + cameraData["focalPoint"] = [ + (pos["position"][i] - self.viewUp[i] + deltaVect[i]) for i in range(3) + ] + yield cameraData + + +# ----------------------------------------------------------------------------- +# MultiView Camera +# ----------------------------------------------------------------------------- + + +class MultiViewCamera(object): + def __init__(self, dataHandler): + self.dataHandler = dataHandler + self.cameraSettings = [] + self.positionNames = [] + + def registerViewPoint(self, name, focalPoint, position, viewUp): + self.cameraSettings.append( + { + "name": name, + "nameIdx": len(self.positionNames), + "focalPoint": focalPoint, + "position": position, + "viewUp": viewUp, + } + ) + self.positionNames.append(name) + self.dataHandler.registerArgument( + priority=0, name="multiView", values=self.positionNames + ) + self.dataHandler.updateBasePattern() + + def updatePriority(self, priorityList): + keyList = ["multiView"] + for idx in range(min(len(priorityList), len(keyList))): + self.dataHandler.updatePriority(keyList[idx], priorityList[idx]) + + def __iter__(self): + for cameraData in self.cameraSettings: + self.dataHandler.setArguments(multiView=cameraData["nameIdx"]) + yield cameraData + + +# ----------------------------------------------------------------------------- +# Helper methods +# ----------------------------------------------------------------------------- + + +def update_camera(renderer, cameraData): + camera = renderer.GetActiveCamera() + camera.SetPosition(cameraData["position"]) + camera.SetFocalPoint(cameraData["focalPoint"]) + camera.SetViewUp(cameraData["viewUp"]) + + +def create_spherical_camera(renderer, dataHandler, phiValues, thetaValues): + camera = renderer.GetActiveCamera() + return SphericalCamera( + dataHandler, + camera.GetFocalPoint(), + camera.GetPosition(), + camera.GetViewUp(), + phiValues, + thetaValues, + ) + + +def create_cylindrical_camera(renderer, dataHandler, phiValues, translationValues): + camera = renderer.GetActiveCamera() + return CylindricalCamera( + dataHandler, + camera.GetFocalPoint(), + camera.GetPosition(), + camera.GetViewUp(), + phiValues, + translationValues, + ) diff --git a/Web/Python/vtkmodules/web/dataset_builder.py b/Web/Python/vtkmodules/web/dataset_builder.py new file mode 100644 index 000000000..5a997b165 --- /dev/null +++ b/Web/Python/vtkmodules/web/dataset_builder.py @@ -0,0 +1,620 @@ +import json, os, gzip, shutil + +from vtkmodules.vtkRenderingCore import vtkWindowToImageFilter +from vtkmodules.vtkIOImage import vtkPNGReader, vtkPNGWriter, vtkJPEGWriter +from vtkmodules.vtkCommonDataModel import vtkImageData +from vtkmodules.vtkCommonCore import vtkUnsignedCharArray +from vtkmodules.vtkFiltersParallel import vtkPResampleFilter + +from vtkmodules.web import iteritems, getJSArrayType +from vtkmodules.web.camera import ( + update_camera, + create_spherical_camera, + create_cylindrical_camera, +) +from vtkmodules.web.query_data_model import DataHandler + +# Global helper variables +encode_codes = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + +# ----------------------------------------------------------------------------- +# Capture image from render window +# ----------------------------------------------------------------------------- + + +class CaptureRenderWindow(object): + def __init__(self, magnification=1): + self.windowToImage = vtkWindowToImageFilter() + self.windowToImage.SetScale(magnification) + self.windowToImage.SetInputBufferTypeToRGB() + self.windowToImage.ReadFrontBufferOn() + self.writer = None + + def SetRenderWindow(self, renderWindow): + self.windowToImage.SetInput(renderWindow) + + def SetFormat(self, mimeType): + if mimeType == "image/png": + self.writer = vtkPNGWriter() + self.writer.SetInputConnection(self.windowToImage.GetOutputPort()) + elif mimeType == "image/jpg": + self.writer = vtkJPEGWriter() + self.writer.SetInputConnection(self.windowToImage.GetOutputPort()) + + def writeImage(self, path): + if self.writer: + self.windowToImage.Modified() + self.windowToImage.Update() + self.writer.SetFileName(path) + self.writer.Write() + + +# ----------------------------------------------------------------------------- +# Basic Dataset Builder +# ----------------------------------------------------------------------------- + + +class DataSetBuilder(object): + def __init__(self, location, camera_data, metadata={}, sections={}): + self.dataHandler = DataHandler(location) + self.cameraDescription = camera_data + self.camera = None + self.imageCapture = CaptureRenderWindow() + + for key, value in iteritems(metadata): + self.dataHandler.addMetaData(key, value) + + for key, value in iteritems(sections): + self.dataHandler.addSection(key, value) + + def getDataHandler(self): + return self.dataHandler + + def getCamera(self): + return self.camera + + def updateCamera(self, camera): + update_camera(self.renderer, camera) + self.renderWindow.Render() + + def start(self, renderWindow=None, renderer=None): + if renderWindow: + # Keep track of renderWindow and renderer + self.renderWindow = renderWindow + self.renderer = renderer + + # Initialize image capture + self.imageCapture.SetRenderWindow(renderWindow) + + # Handle camera if any + if self.cameraDescription: + if self.cameraDescription["type"] == "spherical": + self.camera = create_spherical_camera( + renderer, + self.dataHandler, + self.cameraDescription["phi"], + self.cameraDescription["theta"], + ) + elif self.cameraDescription["type"] == "cylindrical": + self.camera = create_cylindrical_camera( + renderer, + self.dataHandler, + self.cameraDescription["phi"], + self.cameraDescription["translation"], + ) + + # Update background color + bgColor = renderer.GetBackground() + bgColorString = "rgb(%d, %d, %d)" % tuple( + int(bgColor[i] * 255) for i in range(3) + ) + self.dataHandler.addMetaData("backgroundColor", bgColorString) + + # Update file patterns + self.dataHandler.updateBasePattern() + + def stop(self): + self.dataHandler.writeDataDescriptor() + + +# ----------------------------------------------------------------------------- +# Image Dataset Builder +# ----------------------------------------------------------------------------- + + +class ImageDataSetBuilder(DataSetBuilder): + def __init__(self, location, imageMimeType, cameraInfo, metadata={}, sections={}): + DataSetBuilder.__init__(self, location, cameraInfo, metadata, sections) + imageExtenstion = "." + imageMimeType.split("/")[1] + self.dataHandler.registerData( + name="image", type="blob", mimeType=imageMimeType, fileName=imageExtenstion + ) + self.imageCapture.SetFormat(imageMimeType) + + def writeImage(self): + self.imageCapture.writeImage(self.dataHandler.getDataAbsoluteFilePath("image")) + + def writeImages(self): + for cam in self.camera: + update_camera(self.renderer, cam) + self.renderWindow.Render() + self.imageCapture.writeImage( + self.dataHandler.getDataAbsoluteFilePath("image") + ) + + +# ----------------------------------------------------------------------------- +# Volume Composite Dataset Builder +# ----------------------------------------------------------------------------- +class VolumeCompositeDataSetBuilder(DataSetBuilder): + def __init__(self, location, imageMimeType, cameraInfo, metadata={}, sections={}): + DataSetBuilder.__init__(self, location, cameraInfo, metadata, sections) + + self.dataHandler.addTypes("volume-composite", "rgba+depth") + + self.imageMimeType = imageMimeType + self.imageExtenstion = "." + imageMimeType.split("/")[1] + + if imageMimeType == "image/png": + self.imageWriter = vtkPNGWriter() + if imageMimeType == "image/jpg": + self.imageWriter = vtkJPEGWriter() + + self.imageDataColor = vtkImageData() + self.imageWriter.SetInputData(self.imageDataColor) + + self.imageDataDepth = vtkImageData() + self.depthToWrite = None + + self.layerInfo = {} + self.colorByMapping = {} + self.compositePipeline = { + "layers": [], + "dimensions": [], + "fields": {}, + "layer_fields": {}, + "pipeline": [], + } + self.activeDepthKey = "" + self.activeRGBKey = "" + self.nodeWithChildren = {} + + def _getColorCode(self, colorBy): + if colorBy in self.colorByMapping: + # The color code exist + return self.colorByMapping[colorBy] + else: + # No color code assigned yet + colorCode = encode_codes[len(self.colorByMapping)] + # Assign color code + self.colorByMapping[colorBy] = colorCode + # Register color code with color by value + self.compositePipeline["fields"][colorCode] = colorBy + # Return the color code + return colorCode + + def _getLayerCode(self, parent, layerName): + if layerName in self.layerInfo: + # Layer already exist + return (self.layerInfo[layerName]["code"], False) + else: + layerCode = encode_codes[len(self.layerInfo)] + self.layerInfo[layerName] = { + "code": layerCode, + "name": layerName, + "parent": parent, + } + self.compositePipeline["layers"].append(layerCode) + self.compositePipeline["layer_fields"][layerCode] = [] + + # Let's register it in the pipeline + if parent: + if parent not in self.nodeWithChildren: + # Need to create parent + rootNode = {"name": parent, "ids": [], "children": []} + self.nodeWithChildren[parent] = rootNode + self.compositePipeline["pipeline"].append(rootNode) + + # Add node to its parent + self.nodeWithChildren[parent]["children"].append( + {"name": layerName, "ids": [layerCode]} + ) + self.nodeWithChildren[parent]["ids"].append(layerCode) + + else: + self.compositePipeline["pipeline"].append( + {"name": layerName, "ids": [layerCode]} + ) + + return (layerCode, True) + + def _needToRegisterColor(self, layerCode, colorCode): + if colorCode in self.compositePipeline["layer_fields"][layerCode]: + return False + else: + self.compositePipeline["layer_fields"][layerCode].append(colorCode) + return True + + def activateLayer(self, parent, name, colorBy): + layerCode, needToRegisterDepth = self._getLayerCode(parent, name) + colorCode = self._getColorCode(colorBy) + needToRegisterColor = self._needToRegisterColor(layerCode, colorCode) + + # Update active keys + self.activeDepthKey = "%s_depth" % layerCode + self.activeRGBKey = "%s%s_rgb" % (layerCode, colorCode) + + # Need to register data + if needToRegisterDepth: + self.dataHandler.registerData( + name=self.activeDepthKey, + type="array", + fileName="/%s_depth.uint8" % layerCode, + categories=[layerCode], + ) + + if needToRegisterColor: + self.dataHandler.registerData( + name=self.activeRGBKey, + type="blob", + fileName="/%s%s_rgb%s" % (layerCode, colorCode, self.imageExtenstion), + categories=["%s%s" % (layerCode, colorCode)], + mimeType=self.imageMimeType, + ) + + def writeData(self, mapper): + width = self.renderWindow.GetSize()[0] + height = self.renderWindow.GetSize()[1] + + if not self.depthToWrite: + self.depthToWrite = bytearray(width * height) + + for cam in self.camera: + self.updateCamera(cam) + imagePath = self.dataHandler.getDataAbsoluteFilePath(self.activeRGBKey) + depthPath = self.dataHandler.getDataAbsoluteFilePath(self.activeDepthKey) + + # ----------------------------------------------------------------- + # Write Image + # ----------------------------------------------------------------- + mapper.GetColorImage(self.imageDataColor) + self.imageWriter.SetFileName(imagePath) + self.imageWriter.Write() + + # ----------------------------------------------------------------- + # Write Depth + # ----------------------------------------------------------------- + mapper.GetDepthImage(self.imageDataDepth) + inputArray = self.imageDataDepth.GetPointData().GetArray(0) + size = inputArray.GetNumberOfTuples() + for idx in range(size): + self.depthToWrite[idx] = int(inputArray.GetValue(idx)) + + with open(depthPath, "wb") as f: + f.write(self.depthToWrite) + + def start(self, renderWindow, renderer): + DataSetBuilder.start(self, renderWindow, renderer) + self.camera.updatePriority([2, 1]) + + def stop(self, compress=True): + # Push metadata + self.compositePipeline["dimensions"] = self.renderWindow.GetSize() + self.compositePipeline["default_pipeline"] = ( + "A".join(self.compositePipeline["layers"]) + "A" + ) + self.dataHandler.addSection("CompositePipeline", self.compositePipeline) + + # Write metadata + DataSetBuilder.stop(self) + + if compress: + for root, dirs, files in os.walk(self.dataHandler.getBasePath()): + print("Compress", root) + for name in files: + if ".uint8" in name and ".gz" not in name: + with open(os.path.join(root, name), "rb") as f_in: + with gzip.open( + os.path.join(root, name + ".gz"), "wb" + ) as f_out: + shutil.copyfileobj(f_in, f_out) + os.remove(os.path.join(root, name)) + + +# ----------------------------------------------------------------------------- +# Data Prober Dataset Builder +# ----------------------------------------------------------------------------- +class DataProberDataSetBuilder(DataSetBuilder): + def __init__( + self, + location, + sampling_dimesions, + fields_to_keep, + custom_probing_bounds=None, + metadata={}, + ): + DataSetBuilder.__init__(self, location, None, metadata) + self.fieldsToWrite = fields_to_keep + self.resamplerFilter = vtkPResampleFilter() + self.resamplerFilter.SetSamplingDimension(sampling_dimesions) + if custom_probing_bounds: + self.resamplerFilter.SetUseInputBounds(0) + self.resamplerFilter.SetCustomSamplingBounds(custom_probing_bounds) + else: + self.resamplerFilter.SetUseInputBounds(1) + + # Register all fields + self.dataHandler.addTypes("data-prober", "binary") + self.DataProber = { + "types": {}, + "dimensions": sampling_dimesions, + "ranges": {}, + "spacing": [1, 1, 1], + } + for field in self.fieldsToWrite: + self.dataHandler.registerData( + name=field, type="array", fileName="/%s.array" % field + ) + + def setDataToProbe(self, dataset): + self.resamplerFilter.SetInputData(dataset) + + def setSourceToProbe(self, source): + self.resamplerFilter.SetInputConnection(source.GetOutputPort()) + + def writeData(self): + self.resamplerFilter.Update() + arrays = self.resamplerFilter.GetOutput().GetPointData() + for field in self.fieldsToWrite: + array = arrays.GetArray(field) + if array: + b = memoryview(array) + with open(self.dataHandler.getDataAbsoluteFilePath(field), "wb") as f: + f.write(b) + + self.DataProber["types"][field] = getJSArrayType(array) + if field in self.DataProber["ranges"]: + dataRange = array.GetRange() + if dataRange[0] < self.DataProber["ranges"][field][0]: + self.DataProber["ranges"][field][0] = dataRange[0] + if dataRange[1] > self.DataProber["ranges"][field][1]: + self.DataProber["ranges"][field][1] = dataRange[1] + else: + self.DataProber["ranges"][field] = [ + array.GetRange()[0], + array.GetRange()[1], + ] + else: + print("No array for", field) + print(self.resamplerFilter.GetOutput()) + + def stop(self, compress=True): + # Push metadata + self.dataHandler.addSection("DataProber", self.DataProber) + + # Write metadata + DataSetBuilder.stop(self) + + if compress: + for root, dirs, files in os.walk(self.dataHandler.getBasePath()): + print("Compress", root) + for name in files: + if ".array" in name and ".gz" not in name: + with open(os.path.join(root, name), "rb") as f_in: + with gzip.open( + os.path.join(root, name + ".gz"), "wb" + ) as f_out: + shutil.copyfileobj(f_in, f_out) + os.remove(os.path.join(root, name)) + + +# ----------------------------------------------------------------------------- +# Sorted Composite Dataset Builder +# ----------------------------------------------------------------------------- +class ConvertVolumeStackToSortedStack(object): + def __init__(self, width, height): + self.width = width + self.height = height + self.layers = 0 + + def convert(self, directory): + imagePaths = {} + depthPaths = {} + layerNames = [] + for fileName in os.listdir(directory): + if "_rgb" in fileName or "_depth" in fileName: + fileId = fileName.split("_")[0][0] + if "_rgb" in fileName: + imagePaths[fileId] = os.path.join(directory, fileName) + else: + layerNames.append(fileId) + depthPaths[fileId] = os.path.join(directory, fileName) + + layerNames.sort() + + if len(layerNames) == 0: + return + + # Load data in Memory + depthArrays = [] + imageReader = vtkPNGReader() + numberOfValues = self.width * self.height * len(layerNames) + imageSize = self.width * self.height + self.layers = len(layerNames) + + # Write all images as single memoryview + opacity = vtkUnsignedCharArray() + opacity.SetNumberOfComponents(1) + opacity.SetNumberOfTuples(numberOfValues) + + intensity = vtkUnsignedCharArray() + intensity.SetNumberOfComponents(1) + intensity.SetNumberOfTuples(numberOfValues) + + for layer in range(self.layers): + imageReader.SetFileName(imagePaths[layerNames[layer]]) + imageReader.Update() + + rgbaArray = imageReader.GetOutput().GetPointData().GetArray(0) + + for idx in range(imageSize): + intensity.SetValue( + (layer * imageSize) + idx, rgbaArray.GetValue(idx * 4) + ) + opacity.SetValue( + (layer * imageSize) + idx, rgbaArray.GetValue(idx * 4 + 3) + ) + + with open(depthPaths[layerNames[layer]], "rb") as depthFile: + depthArrays.append(depthFile.read()) + + # Apply pixel sorting + destOrder = vtkUnsignedCharArray() + destOrder.SetNumberOfComponents(1) + destOrder.SetNumberOfTuples(numberOfValues) + + opacityOrder = vtkUnsignedCharArray() + opacityOrder.SetNumberOfComponents(1) + opacityOrder.SetNumberOfTuples(numberOfValues) + + intensityOrder = vtkUnsignedCharArray() + intensityOrder.SetNumberOfComponents(1) + intensityOrder.SetNumberOfTuples(numberOfValues) + + for pixelIdx in range(imageSize): + depthStack = [] + for depthArray in depthArrays: + depthStack.append((depthArray[pixelIdx], len(depthStack))) + depthStack.sort(key=lambda tup: tup[0]) + + for destLayerIdx in range(len(depthStack)): + sourceLayerIdx = depthStack[destLayerIdx][1] + + # Copy Idx + destOrder.SetValue( + (imageSize * destLayerIdx) + pixelIdx, sourceLayerIdx + ) + opacityOrder.SetValue( + (imageSize * destLayerIdx) + pixelIdx, + opacity.GetValue((imageSize * sourceLayerIdx) + pixelIdx), + ) + intensityOrder.SetValue( + (imageSize * destLayerIdx) + pixelIdx, + intensity.GetValue((imageSize * sourceLayerIdx) + pixelIdx), + ) + + with open(os.path.join(directory, "alpha.uint8"), "wb") as f: + f.write(memoryview(opacityOrder)) + + with open(os.path.join(directory, "intensity.uint8"), "wb") as f: + f.write(memoryview(intensityOrder)) + + with open(os.path.join(directory, "order.uint8"), "wb") as f: + f.write(memoryview(destOrder)) + + +class SortedCompositeDataSetBuilder(VolumeCompositeDataSetBuilder): + def __init__(self, location, cameraInfo, metadata={}, sections={}): + VolumeCompositeDataSetBuilder.__init__( + self, location, "image/png", cameraInfo, metadata, sections + ) + self.dataHandler.addTypes("sorted-composite", "rgba") + + # Register order and color textures + self.layerScalars = [] + self.dataHandler.registerData( + name="order", type="array", fileName="/order.uint8" + ) + self.dataHandler.registerData( + name="alpha", type="array", fileName="/alpha.uint8" + ) + self.dataHandler.registerData( + name="intensity", + type="array", + fileName="/intensity.uint8", + categories=["intensity"], + ) + + def start(self, renderWindow, renderer): + VolumeCompositeDataSetBuilder.start(self, renderWindow, renderer) + imageSize = self.renderWindow.GetSize() + self.dataConverter = ConvertVolumeStackToSortedStack(imageSize[0], imageSize[1]) + + def activateLayer(self, colorBy, scalar): + VolumeCompositeDataSetBuilder.activateLayer( + self, "root", "%s" % scalar, colorBy + ) + self.layerScalars.append(scalar) + + def writeData(self, mapper): + VolumeCompositeDataSetBuilder.writeData(self, mapper) + + # Fill data pattern + self.dataHandler.getDataAbsoluteFilePath("order") + self.dataHandler.getDataAbsoluteFilePath("alpha") + self.dataHandler.getDataAbsoluteFilePath("intensity") + + def stop(self, clean=True, compress=True): + VolumeCompositeDataSetBuilder.stop(self, compress=False) + + # Go through all directories and convert them + for root, dirs, files in os.walk(self.dataHandler.getBasePath()): + for name in dirs: + print("Process", os.path.join(root, name)) + self.dataConverter.convert(os.path.join(root, name)) + + # Rename index.json to info_origin.json + os.rename( + os.path.join(self.dataHandler.getBasePath(), "index.json"), + os.path.join(self.dataHandler.getBasePath(), "index_origin.json"), + ) + + # Update index.json + with open( + os.path.join(self.dataHandler.getBasePath(), "index_origin.json"), "r" + ) as infoFile: + metadata = json.load(infoFile) + metadata["SortedComposite"] = { + "dimensions": metadata["CompositePipeline"]["dimensions"], + "layers": self.dataConverter.layers, + "scalars": self.layerScalars[0 : self.dataConverter.layers], + } + + # Clean metadata + dataToKeep = [] + del metadata["CompositePipeline"] + for item in metadata["data"]: + if item["name"] in ["order", "alpha", "intensity"]: + dataToKeep.append(item) + metadata["data"] = dataToKeep + metadata["type"] = ["tonic-query-data-model", "sorted-composite", "alpha"] + + # Override index.json + with open( + os.path.join(self.dataHandler.getBasePath(), "index.json"), "w" + ) as newMetaFile: + newMetaFile.write(json.dumps(metadata)) + + # Clean temporary data + if clean: + for root, dirs, files in os.walk(self.dataHandler.getBasePath()): + print("Clean", root) + for name in files: + if ( + "_rgb.png" in name + or "_depth.uint8" in name + or name == "index_origin.json" + ): + os.remove(os.path.join(root, name)) + + if compress: + for root, dirs, files in os.walk(self.dataHandler.getBasePath()): + print("Compress", root) + for name in files: + if ".uint8" in name and ".gz" not in name: + with open(os.path.join(root, name), "rb") as f_in: + with gzip.open( + os.path.join(root, name + ".gz"), "wb" + ) as f_out: + shutil.copyfileobj(f_in, f_out) + os.remove(os.path.join(root, name)) diff --git a/Web/Python/vtkmodules/web/errors.py b/Web/Python/vtkmodules/web/errors.py new file mode 100644 index 000000000..3d8e442b3 --- /dev/null +++ b/Web/Python/vtkmodules/web/errors.py @@ -0,0 +1,12 @@ +WEB_DEPENDENCY_MISSING_MESSAGE = """Please install VTK's Web module dependencies. + +These include `wslink` and can be easily installed with vtk by using the +`web` extra requirements option. For example: + + pip install vtk[web] + +""" + +class WebDependencyMissingError(ImportError): + def __init__(self, message=WEB_DEPENDENCY_MISSING_MESSAGE): + super().__init__(message) diff --git a/Web/Python/vtkmodules/web/protocols.py b/Web/Python/vtkmodules/web/protocols.py new file mode 100644 index 000000000..312bb3169 --- /dev/null +++ b/Web/Python/vtkmodules/web/protocols.py @@ -0,0 +1,842 @@ +r"""protocols is a module that contains a set of VTK Web related +protocols that can be combined together to provide a flexible way to define +very specific web application. +""" + +from __future__ import absolute_import, division, print_function + +import os, sys, logging, types, inspect, traceback, re, base64, time + +from vtkmodules.vtkWebCore import vtkWebInteractionEvent + +from vtkmodules.web.errors import WebDependencyMissingError +from vtkmodules.web.render_window_serializer import ( + serializeInstance, + SynchronizationContext, + getReferenceId, + initializeSerializers, +) + +try: + from wslink import schedule_callback + from wslink import register as exportRpc + from wslink.websocket import LinkProtocol +except ImportError: + raise WebDependencyMissingError() + +# ============================================================================= +# +# Base class for any VTK Web based protocol +# +# ============================================================================= + + +class vtkWebProtocol(LinkProtocol): + def getApplication(self): + return self.getSharedObject("app") + + # no need for a setApplication anymore, but keep for compatibility + def setApplication(self, app): + pass + + def mapIdToObject(self, id): + """ + Maps global-id for a vtkObject to the vtkObject instance. May return None if the + id is not valid. + """ + id = int(id) + if id <= 0: + return None + return self.getApplication().GetObjectIdMap().GetVTKObject(id) + + def getGlobalId(self, obj): + """ + Return the id for a given vtkObject + """ + return self.getApplication().GetObjectIdMap().GetGlobalId(obj) + + def freeObject(self, obj): + """ + Delete the given vtkObject from the objectIdMap. Returns true if delete succeeded. + """ + return self.getApplication().GetObjectIdMap().FreeObject(obj) + + def freeObjectById(self, id): + """ + Delete the vtkObject corresponding to the given objectId from the objectIdMap. + Returns true if delete succeeded. + """ + return self.getApplication().GetObjectIdMap().FreeObjectById(id) + + def getView(self, vid): + """ + Returns the view for a given view ID, if vid is None then return the + current active view. + :param vid: The view ID + :type vid: str + """ + v = self.mapIdToObject(vid) + + if not v: + # Use active view is none provided. + v = self.getApplication().GetObjectIdMap().GetActiveObject("VIEW") + if not v: + raise Exception("no view provided: %s" % vid) + + return v + + def setActiveView(self, view): + """ + Set a vtkRenderWindow to be the active one + """ + self.getApplication().GetObjectIdMap().SetActiveObject("VIEW", view) + + +# ============================================================================= +# +# Handle Mouse interaction on any type of view +# +# ============================================================================= + + +class vtkWebMouseHandler(vtkWebProtocol): + @exportRpc("viewport.mouse.interaction") + def mouseInteraction(self, event): + """ + RPC Callback for mouse interactions. + """ + view = self.getView(event["view"]) + + buttons = 0 + if event["buttonLeft"]: + buttons |= vtkWebInteractionEvent.LEFT_BUTTON + if event["buttonMiddle"]: + buttons |= vtkWebInteractionEvent.MIDDLE_BUTTON + if event["buttonRight"]: + buttons |= vtkWebInteractionEvent.RIGHT_BUTTON + + modifiers = 0 + if event["shiftKey"]: + modifiers |= vtkWebInteractionEvent.SHIFT_KEY + if event["ctrlKey"]: + modifiers |= vtkWebInteractionEvent.CTRL_KEY + if event["altKey"]: + modifiers |= vtkWebInteractionEvent.ALT_KEY + if event["metaKey"]: + modifiers |= vtkWebInteractionEvent.META_KEY + + pvevent = vtkWebInteractionEvent() + pvevent.SetButtons(buttons) + pvevent.SetModifiers(modifiers) + if "x" in event: + pvevent.SetX(event["x"]) + if "y" in event: + pvevent.SetY(event["y"]) + if "scroll" in event: + pvevent.SetScroll(event["scroll"]) + if event["action"] == "dblclick": + pvevent.SetRepeatCount(2) + # pvevent.SetKeyCode(event["charCode"]) + retVal = self.getApplication().HandleInteractionEvent(view, pvevent) + del pvevent + + if event["action"] == "down": + self.getApplication().InvokeEvent("StartInteractionEvent") + + if event["action"] == "up": + self.getApplication().InvokeEvent("EndInteractionEvent") + + if retVal: + self.getApplication().InvokeEvent("UpdateEvent") + + return retVal + + @exportRpc("viewport.mouse.zoom.wheel") + def updateZoomFromWheel(self, event): + if "Start" in event["type"]: + self.getApplication().InvokeEvent("StartInteractionEvent") + + renderWindow = self.getView(event["view"]) + if renderWindow and "spinY" in event: + zoomFactor = 1.0 - event["spinY"] / 10.0 + + camera = renderWindow.GetRenderers().GetFirstRenderer().GetActiveCamera() + fp = camera.GetFocalPoint() + pos = camera.GetPosition() + delta = [fp[i] - pos[i] for i in range(3)] + camera.Zoom(zoomFactor) + + pos2 = camera.GetPosition() + camera.SetFocalPoint([pos2[i] + delta[i] for i in range(3)]) + renderWindow.Modified() + + if "End" in event["type"]: + self.getApplication().InvokeEvent("EndInteractionEvent") + + +# ============================================================================= +# +# Basic 3D Viewport API (Camera + Orientation + CenterOfRotation +# +# ============================================================================= + + +class vtkWebViewPort(vtkWebProtocol): + @exportRpc("viewport.camera.reset") + def resetCamera(self, viewId): + """ + RPC callback to reset camera. + """ + view = self.getView(viewId) + renderer = view.GetRenderers().GetFirstRenderer() + renderer.ResetCamera() + + self.getApplication().InvalidateCache(view) + self.getApplication().InvokeEvent("UpdateEvent") + + return str(self.getGlobalId(view)) + + @exportRpc("viewport.axes.orientation.visibility.update") + def updateOrientationAxesVisibility(self, viewId, showAxis): + """ + RPC callback to show/hide OrientationAxis. + """ + view = self.getView(viewId) + # FIXME seb: view.OrientationAxesVisibility = (showAxis if 1 else 0); + + self.getApplication().InvalidateCache(view) + self.getApplication().InvokeEvent("UpdateEvent") + + return str(self.getGlobalId(view)) + + @exportRpc("viewport.axes.center.visibility.update") + def updateCenterAxesVisibility(self, viewId, showAxis): + """ + RPC callback to show/hide CenterAxesVisibility. + """ + view = self.getView(viewId) + # FIXME seb: view.CenterAxesVisibility = (showAxis if 1 else 0); + + self.getApplication().InvalidateCache(view) + self.getApplication().InvokeEvent("UpdateEvent") + + return str(self.getGlobalId(view)) + + @exportRpc("viewport.camera.update") + def updateCamera(self, view_id, focal_point, view_up, position, forceUpdate=True): + view = self.getView(view_id) + + camera = view.GetRenderers().GetFirstRenderer().GetActiveCamera() + camera.SetFocalPoint(focal_point) + camera.SetViewUp(view_up) + camera.SetPosition(position) + + if forceUpdate: + self.getApplication().InvalidateCache(view) + self.getApplication().InvokeEvent("UpdateEvent") + + +# ============================================================================= +# +# Provide Image delivery mechanism (deprecated - will be removed in VTK 10+) +# +# ============================================================================= + + +class vtkWebViewPortImageDelivery(vtkWebProtocol): + @exportRpc("viewport.image.render") + def stillRender(self, options): + """ + RPC Callback to render a view and obtain the rendered image. + """ + beginTime = int(round(time.time() * 1000)) + view = self.getView(options["view"]) + size = [view.GetSize()[0], view.GetSize()[1]] + # use existing size, overridden only if options["size"] is set. + resize = size != options.get("size", size) + if resize: + size = options["size"] + if size[0] > 0 and size[1] > 0: + view.SetSize(size) + t = 0 + if options and "mtime" in options: + t = options["mtime"] + quality = 100 + if options and "quality" in options: + quality = options["quality"] + localTime = 0 + if options and "localTime" in options: + localTime = options["localTime"] + reply = {} + app = self.getApplication() + if t == 0: + app.InvalidateCache(view) + reply["image"] = app.StillRenderToString(view, t, quality) + # Check that we are getting image size we have set. If not, wait until we + # do. The render call will set the actual window size. + tries = 10 + while resize and list(view.GetSize()) != size and size != [0, 0] and tries > 0: + app.InvalidateCache(view) + reply["image"] = app.StillRenderToString(view, t, quality) + tries -= 1 + + reply["stale"] = app.GetHasImagesBeingProcessed(view) + reply["mtime"] = app.GetLastStillRenderToMTime() + reply["size"] = [view.GetSize()[0], view.GetSize()[1]] + reply["format"] = "jpeg;base64" + reply["global_id"] = str(self.getGlobalId(view)) + reply["localTime"] = localTime + + endTime = int(round(time.time() * 1000)) + reply["workTime"] = endTime - beginTime + + return reply + + +# ============================================================================= +# +# Provide publish-based Image delivery mechanism +# +# ============================================================================= + + +class vtkWebPublishImageDelivery(vtkWebProtocol): + def __init__(self, decode=True): + super(vtkWebPublishImageDelivery, self).__init__() + self.trackingViews = {} + self.lastStaleTime = 0 + self.staleHandlerCount = 0 + self.deltaStaleTimeBeforeRender = 0.5 # 0.5s + self.decode = decode + self.viewsInAnimations = [] + self.targetFrameRate = 30.0 + self.minFrameRate = 12.0 + self.maxFrameRate = 30.0 + + def pushRender(self, vId, ignoreAnimation=False): + if vId not in self.trackingViews: + return + + if not self.trackingViews[vId]["enabled"]: + return + + if not ignoreAnimation and len(self.viewsInAnimations) > 0: + return + + if "originalSize" not in self.trackingViews[vId]: + view = self.getView(vId) + self.trackingViews[vId]["originalSize"] = list(view.GetSize()) + + if "ratio" not in self.trackingViews[vId]: + self.trackingViews[vId]["ratio"] = 1 + + ratio = self.trackingViews[vId]["ratio"] + mtime = self.trackingViews[vId]["mtime"] + quality = self.trackingViews[vId]["quality"] + size = [int(s * ratio) for s in self.trackingViews[vId]["originalSize"]] + + reply = self.stillRender( + {"view": vId, "mtime": mtime, "quality": quality, "size": size} + ) + stale = reply["stale"] + if reply["image"]: + # depending on whether the app has encoding enabled: + if self.decode: + reply["image"] = base64.standard_b64decode(reply["image"]) + + reply["image"] = self.addAttachment(reply["image"]) + reply["format"] = "jpeg" + # save mtime for next call. + self.trackingViews[vId]["mtime"] = reply["mtime"] + # echo back real ID, instead of -1 for 'active' + reply["id"] = vId + self.publish("viewport.image.push.subscription", reply) + if stale: + self.lastStaleTime = time.time() + if self.staleHandlerCount == 0: + self.staleHandlerCount += 1 + schedule_callback( + self.deltaStaleTimeBeforeRender, lambda: self.renderStaleImage(vId) + ) + else: + self.lastStaleTime = 0 + + def renderStaleImage(self, vId): + self.staleHandlerCount -= 1 + + if self.lastStaleTime != 0: + delta = time.time() - self.lastStaleTime + if delta >= self.deltaStaleTimeBeforeRender: + self.pushRender(vId) + else: + self.staleHandlerCount += 1 + schedule_callback( + self.deltaStaleTimeBeforeRender - delta + 0.001, + lambda: self.renderStaleImage(vId), + ) + + def animate(self): + if len(self.viewsInAnimations) == 0: + return + + nextAnimateTime = time.time() + 1.0 / self.targetFrameRate + for vId in self.viewsInAnimations: + self.pushRender(vId, True) + + nextAnimateTime -= time.time() + + if self.targetFrameRate > self.maxFrameRate: + self.targetFrameRate = self.maxFrameRate + + if nextAnimateTime < 0: + if nextAnimateTime < -1.0: + self.targetFrameRate = 1 + if self.targetFrameRate > self.minFrameRate: + self.targetFrameRate -= 1.0 + schedule_callback(0.001, lambda: self.animate()) + else: + if self.targetFrameRate < self.maxFrameRate and nextAnimateTime > 0.005: + self.targetFrameRate += 1.0 + schedule_callback(nextAnimateTime, lambda: self.animate()) + + @exportRpc("viewport.image.animation.fps.max") + def setMaxFrameRate(self, fps=30): + self.maxFrameRate = fps + + @exportRpc("viewport.image.animation.fps.get") + def getCurrentFrameRate(self): + return self.targetFrameRate + + @exportRpc("viewport.image.animation.start") + def startViewAnimation(self, viewId="-1"): + sView = self.getView(viewId) + realViewId = str(self.getGlobalId(sView)) + + self.viewsInAnimations.append(realViewId) + if len(self.viewsInAnimations) == 1: + self.animate() + + @exportRpc("viewport.image.animation.stop") + def stopViewAnimation(self, viewId="-1"): + sView = self.getView(viewId) + realViewId = str(self.getGlobalId(sView)) + + if realViewId in self.viewsInAnimations: + self.viewsInAnimations.remove(realViewId) + + @exportRpc("viewport.image.push") + def imagePush(self, options): + sView = self.getView(options["view"]) + realViewId = str(self.getGlobalId(sView)) + # Make sure an image is pushed + self.getApplication().InvalidateCache(sView) + self.pushRender(realViewId) + + # Internal function since the reply[image] is not + # JSON(serializable) it can not be an RPC one + def stillRender(self, options): + """ + RPC Callback to render a view and obtain the rendered image. + """ + beginTime = int(round(time.time() * 1000)) + view = self.getView(options["view"]) + size = view.GetSize()[0:2] + resize = size != options.get("size", size) + if resize: + size = options["size"] + if size[0] > 10 and size[1] > 10: + view.SetSize(size) + t = 0 + if options and "mtime" in options: + t = options["mtime"] + quality = 100 + if options and "quality" in options: + quality = options["quality"] + localTime = 0 + if options and "localTime" in options: + localTime = options["localTime"] + reply = {} + app = self.getApplication() + if t == 0: + app.InvalidateCache(view) + if self.decode: + stillRender = app.StillRenderToString + else: + stillRender = app.StillRenderToBuffer + reply_image = stillRender(view, t, quality) + + # Check that we are getting image size we have set if not wait until we + # do. The render call will set the actual window size. + tries = 10 + while resize and list(view.GetSize()) != size and size != [0, 0] and tries > 0: + app.InvalidateCache(view) + reply_image = stillRender(view, t, quality) + tries -= 1 + + if ( + not resize + and options + and ("clearCache" in options) + and options["clearCache"] + ): + app.InvalidateCache(view) + reply_image = stillRender(view, t, quality) + + reply["stale"] = app.GetHasImagesBeingProcessed(view) + reply["mtime"] = app.GetLastStillRenderToMTime() + reply["size"] = view.GetSize()[0:2] + reply["memsize"] = reply_image.GetDataSize() if reply_image else 0 + reply["format"] = "jpeg;base64" if self.decode else "jpeg" + reply["global_id"] = str(self.getGlobalId(view)) + reply["localTime"] = localTime + if self.decode: + reply["image"] = reply_image + else: + # Convert the vtkUnsignedCharArray into a bytes object, required by Autobahn websockets + reply["image"] = memoryview(reply_image).tobytes() if reply_image else None + + endTime = int(round(time.time() * 1000)) + reply["workTime"] = endTime - beginTime + + return reply + + @exportRpc("viewport.image.push.observer.add") + def addRenderObserver(self, viewId): + sView = self.getView(viewId) + if not sView: + return {"error": "Unable to get view with id %s" % viewId} + + realViewId = str(self.getGlobalId(sView)) + + if not realViewId in self.trackingViews: + observerCallback = lambda *args, **kwargs: self.pushRender(realViewId) + startCallback = lambda *args, **kwargs: self.startViewAnimation(realViewId) + stopCallback = lambda *args, **kwargs: self.stopViewAnimation(realViewId) + tag = self.getApplication().AddObserver("UpdateEvent", observerCallback) + tagStart = self.getApplication().AddObserver( + "StartInteractionEvent", startCallback + ) + tagStop = self.getApplication().AddObserver( + "EndInteractionEvent", stopCallback + ) + # TODO do we need self.getApplication().AddObserver('ResetActiveView', resetActiveView()) + self.trackingViews[realViewId] = { + "tags": [tag, tagStart, tagStop], + "observerCount": 1, + "mtime": 0, + "enabled": True, + "quality": 100, + } + else: + # There is an observer on this view already + self.trackingViews[realViewId]["observerCount"] += 1 + + self.pushRender(realViewId) + return {"success": True, "viewId": realViewId} + + @exportRpc("viewport.image.push.observer.remove") + def removeRenderObserver(self, viewId): + sView = self.getView(viewId) + if not sView: + return {"error": "Unable to get view with id %s" % viewId} + + realViewId = str(self.getGlobalId(sView)) + + observerInfo = None + if realViewId in self.trackingViews: + observerInfo = self.trackingViews[realViewId] + + if not observerInfo: + return {"error": "Unable to find subscription for view %s" % realViewId} + + observerInfo["observerCount"] -= 1 + + if observerInfo["observerCount"] <= 0: + for tag in observerInfo["tags"]: + self.getApplication().RemoveObserver(tag) + del self.trackingViews[realViewId] + + return {"result": "success"} + + @exportRpc("viewport.image.push.quality") + def setViewQuality(self, viewId, quality, ratio=1): + sView = self.getView(viewId) + if not sView: + return {"error": "Unable to get view with id %s" % viewId} + + realViewId = str(self.getGlobalId(sView)) + observerInfo = None + if realViewId in self.trackingViews: + observerInfo = self.trackingViews[realViewId] + + if not observerInfo: + return {"error": "Unable to find subscription for view %s" % realViewId} + + observerInfo["quality"] = quality + observerInfo["ratio"] = ratio + + # Update image size right now! + if "originalSize" in self.trackingViews[realViewId]: + size = [ + int(s * ratio) for s in self.trackingViews[realViewId]["originalSize"] + ] + if hasattr(sView, "SetSize"): + sView.SetSize(size) + else: + sView.ViewSize = size + + return {"result": "success"} + + @exportRpc("viewport.image.push.original.size") + def setViewSize(self, viewId, width, height): + sView = self.getView(viewId) + if not sView: + return {"error": "Unable to get view with id %s" % viewId} + + realViewId = str(self.getGlobalId(sView)) + observerInfo = None + if realViewId in self.trackingViews: + observerInfo = self.trackingViews[realViewId] + + if not observerInfo: + return {"error": "Unable to find subscription for view %s" % realViewId} + + observerInfo["originalSize"] = [width, height] + + return {"result": "success"} + + @exportRpc("viewport.image.push.enabled") + def enableView(self, viewId, enabled): + sView = self.getView(viewId) + if not sView: + return {"error": "Unable to get view with id %s" % viewId} + + realViewId = str(self.getGlobalId(sView)) + observerInfo = None + if realViewId in self.trackingViews: + observerInfo = self.trackingViews[realViewId] + + if not observerInfo: + return {"error": "Unable to find subscription for view %s" % realViewId} + + observerInfo["enabled"] = enabled + + return {"result": "success"} + + @exportRpc("viewport.image.push.invalidate.cache") + def invalidateCache(self, viewId): + sView = self.getView(viewId) + if not sView: + return {"error": "Unable to get view with id %s" % viewId} + + self.getApplication().InvalidateCache(sView) + self.getApplication().InvokeEvent("UpdateEvent") + return {"result": "success"} + + +# ============================================================================= +# +# Provide Geometry delivery mechanism (WebGL) (deprecated - will be removed in VTK 10+) +# +# ============================================================================= + + +class vtkWebViewPortGeometryDelivery(vtkWebProtocol): + @exportRpc("viewport.webgl.metadata") + def getSceneMetaData(self, view_id): + view = self.getView(view_id) + data = self.getApplication().GetWebGLSceneMetaData(view) + return data + + @exportRpc("viewport.webgl.data") + def getWebGLData(self, view_id, object_id, part): + view = self.getView(view_id) + data = self.getApplication().GetWebGLBinaryData(view, str(object_id), part - 1) + return data + + +# ============================================================================= +# +# Provide File/Directory listing +# +# ============================================================================= + + +class vtkWebFileBrowser(vtkWebProtocol): + def __init__( + self, basePath, name, excludeRegex=r"^\.|~$|^\$", groupRegex=r"[0-9]+\." + ): + """ + Configure the way the WebFile browser will expose the server content. + - basePath: specify the base directory that we should start with + - name: Name of that base directory that will show up on the web + - excludeRegex: Regular expression of what should be excluded from the list of files/directories + """ + self.baseDirectory = basePath + self.rootName = name + self.pattern = re.compile(excludeRegex) + self.gPattern = re.compile(groupRegex) + + @exportRpc("file.server.directory.list") + def listServerDirectory(self, relativeDir="."): + """ + RPC Callback to list a server directory relative to the basePath + provided at start-up. + """ + path = [self.rootName] + if len(relativeDir) > len(self.rootName): + relativeDir = relativeDir[len(self.rootName) + 1 :] + path += relativeDir.replace("\\", "/").split("/") + + currentPath = os.path.join(self.baseDirectory, relativeDir) + result = { + "label": relativeDir, + "files": [], + "dirs": [], + "groups": [], + "path": path, + } + if relativeDir == ".": + result["label"] = self.rootName + for file in os.listdir(currentPath): + if os.path.isfile(os.path.join(currentPath, file)) and not re.search( + self.pattern, file + ): + result["files"].append({"label": file, "size": -1}) + elif os.path.isdir(os.path.join(currentPath, file)) and not re.search( + self.pattern, file + ): + result["dirs"].append(file) + + # Filter files to create groups + files = result["files"] + files.sort() + groups = result["groups"] + groupIdx = {} + filesToRemove = [] + for file in files: + fileSplit = re.split(self.gPattern, file["label"]) + if len(fileSplit) == 2: + filesToRemove.append(file) + gName = "*.".join(fileSplit) + if gName in groupIdx: + groupIdx[gName]["files"].append(file["label"]) + else: + groupIdx[gName] = {"files": [file["label"]], "label": gName} + groups.append(groupIdx[gName]) + for file in filesToRemove: + gName = "*.".join(re.split(self.gPattern, file["label"])) + if len(groupIdx[gName]["files"]) > 1: + files.remove(file) + else: + groups.remove(groupIdx[gName]) + + return result + + +# ============================================================================= +# +# Provide an updated geometry delivery mechanism which better matches the +# client-side rendering capability we have in vtk.js +# +# ============================================================================= + + +class vtkWebLocalRendering(vtkWebProtocol): + def __init__(self, **kwargs): + super(vtkWebLocalRendering, self).__init__() + initializeSerializers() + self.context = SynchronizationContext() + self.trackingViews = {} + self.mtime = 0 + + # RpcName: getArray => viewport.geometry.array.get + @exportRpc("viewport.geometry.array.get") + def getArray(self, dataHash, binary=False): + if binary: + return self.addAttachment(self.context.getCachedDataArray(dataHash, binary)) + return self.context.getCachedDataArray(dataHash, binary) + + # RpcName: addViewObserver => viewport.geometry.view.observer.add + @exportRpc("viewport.geometry.view.observer.add") + def addViewObserver(self, viewId): + sView = self.getView(viewId) + if not sView: + return {"error": "Unable to get view with id %s" % viewId} + + realViewId = self.getApplication().GetObjectIdMap().GetGlobalId(sView) + + def pushGeometry(newSubscription=False): + stateToReturn = self.getViewState(realViewId, newSubscription) + stateToReturn["mtime"] = 0 if newSubscription else self.mtime + self.mtime += 1 + return stateToReturn + + if not realViewId in self.trackingViews: + observerCallback = lambda *args, **kwargs: self.publish( + "viewport.geometry.view.subscription", pushGeometry() + ) + tag = self.getApplication().AddObserver("UpdateEvent", observerCallback) + self.trackingViews[realViewId] = {"tags": [tag], "observerCount": 1} + else: + # There is an observer on this view already + self.trackingViews[realViewId]["observerCount"] += 1 + + self.publish("viewport.geometry.view.subscription", pushGeometry(True)) + return {"success": True, "viewId": realViewId} + + # RpcName: removeViewObserver => viewport.geometry.view.observer.remove + @exportRpc("viewport.geometry.view.observer.remove") + def removeViewObserver(self, viewId): + sView = self.getView(viewId) + if not sView: + return {"error": "Unable to get view with id %s" % viewId} + + realViewId = self.getApplication().GetObjectIdMap().GetGlobalId(sView) + + observerInfo = None + if realViewId in self.trackingViews: + observerInfo = self.trackingViews[realViewId] + + if not observerInfo: + return {"error": "Unable to find subscription for view %s" % realViewId} + + observerInfo["observerCount"] -= 1 + + if observerInfo["observerCount"] <= 0: + for tag in observerInfo["tags"]: + self.getApplication().RemoveObserver(tag) + del self.trackingViews[realViewId] + + return {"result": "success"} + + # RpcName: getViewState => viewport.geometry.view.get.state + @exportRpc("viewport.geometry.view.get.state") + def getViewState(self, viewId, newSubscription=False): + sView = self.getView(viewId) + if not sView: + return {"error": "Unable to get view with id %s" % viewId} + + self.context.setIgnoreLastDependencies(newSubscription) + + # Get the active view and render window, use it to iterate over renderers + renderWindow = sView + renderer = renderWindow.GetRenderers().GetFirstRenderer() + camera = renderer.GetActiveCamera() + renderWindowId = self.getApplication().GetObjectIdMap().GetGlobalId(sView) + viewInstance = serializeInstance( + None, renderWindow, renderWindowId, self.context, 1 + ) + viewInstance["extra"] = { + "vtkRefId": getReferenceId(renderWindow), + "centerOfRotation": camera.GetFocalPoint(), + "camera": getReferenceId(camera), + } + + self.context.setIgnoreLastDependencies(False) + self.context.checkForArraysToRelease() + + if viewInstance: + return viewInstance + + return None diff --git a/Web/Python/vtkmodules/web/query_data_model.py b/Web/Python/vtkmodules/web/query_data_model.py new file mode 100644 index 000000000..0ac388ffa --- /dev/null +++ b/Web/Python/vtkmodules/web/query_data_model.py @@ -0,0 +1,182 @@ +""" +Core Module for Web Base Data Generation +""" + +import sys, os, json + +from vtkmodules.web import iteritems + + +class DataHandler(object): + def __init__(self, basePath): + self.__root = basePath + self.types = ["tonic-query-data-model"] + self.metadata = {} + self.data = {} + self.arguments = {} + self.current = {} + self.sections = {} + self.basePattern = None + self.priority = [] + self.argOrder = [] + self.realValues = {} + self.can_write = True + + def getBasePath(self): + return self.__root + + def updateBasePattern(self): + self.priority.sort(key=lambda item: item[1]) + self.basePattern = "" + patternSeparator = "" + currentPriority = -1 + + for item in self.priority: + if currentPriority != -1: + if currentPriority == item[1]: + patternSeparator = "_" + else: + patternSeparator = "/" + currentPriority = item[1] + self.basePattern = "{%s}%s%s" % ( + item[0], + patternSeparator, + self.basePattern, + ) + + def registerArgument(self, **kwargs): + """ + We expect the following set of arguments + - priority + - name + - label (optional) + - values + - uiType + - defaultIdx + """ + newArgument = {} + argName = kwargs["name"] + self.argOrder.append(argName) + for key, value in iteritems(kwargs): + if key == "priority": + self.priority.append([argName, value]) + elif key == "values": + self.realValues[argName] = value + newArgument[key] = ["{value}".format(value=x) for x in value] + else: + newArgument[key] = value + + self.arguments[argName] = newArgument + + def updatePriority(self, argumentName, newPriority): + for item in self.priority: + if item[0] == argumentName: + item[1] = newPriority + + def setArguments(self, **kwargs): + """ + Update the arguments index + """ + for key, value in iteritems(kwargs): + self.current[key] = value + + def removeData(self, name): + del self.data[name] + + def registerData(self, **kwargs): + """ + name, type, mimeType, fileName, dependencies + """ + newData = {"metadata": {}} + argName = kwargs["name"] + for key, value in iteritems(kwargs): + if key == "fileName": + if "rootFile" in kwargs and kwargs["rootFile"]: + newData["pattern"] = "{pattern}/%s" % value + else: + newData["pattern"] = "{pattern}%s" % value + else: + newData[key] = value + + self.data[argName] = newData + + def addDataMetaData(self, name, key, value): + self.data[name]["metadata"][key] = value + + def getDataAbsoluteFilePath(self, name, createDirectories=True): + dataPattern = self.data[name]["pattern"] + if "{pattern}" in dataPattern: + if len(self.basePattern) == 0: + dataPattern = dataPattern.replace( + "{pattern}/", self.basePattern + ).replace("{pattern}", self.basePattern) + self.data[name]["pattern"] = dataPattern + else: + dataPattern = dataPattern.replace("{pattern}", self.basePattern) + self.data[name]["pattern"] = dataPattern + + keyValuePair = {} + for key, value in iteritems(self.current): + keyValuePair[key] = self.arguments[key]["values"][value] + + fullpath = os.path.join(self.__root, dataPattern.format(**keyValuePair)) + + if createDirectories and self.can_write: + if not os.path.exists(os.path.dirname(fullpath)): + os.makedirs(os.path.dirname(fullpath)) + + return fullpath + + def addTypes(self, *args): + for arg in args: + self.types.append(arg) + + def addMetaData(self, key, value): + self.metadata[key] = value + + def addSection(self, key, value): + self.sections[key] = value + + def computeDataPatterns(self): + if self.basePattern == None: + self.updateBasePattern() + + for name in self.data: + dataPattern = self.data[name]["pattern"] + if "{pattern}" in dataPattern: + dataPattern = dataPattern.replace("{pattern}", self.basePattern) + self.data[name]["pattern"] = dataPattern + + def __getattr__(self, name): + if self.basePattern == None: + self.updateBasePattern() + + for i in range(len(self.arguments[name]["values"])): + self.current[name] = i + yield self.realValues[name][i] + + def writeDataDescriptor(self): + if not self.can_write: + return + + self.computeDataPatterns() + + jsonData = { + "arguments_order": self.argOrder, + "type": self.types, + "arguments": self.arguments, + "metadata": self.metadata, + "data": [], + } + + # Add sections + for key, value in iteritems(self.sections): + jsonData[key] = value + + # Add data + for key, value in iteritems(self.data): + jsonData["data"].append(value) + + filePathToWrite = os.path.join(self.__root, "index.json") + with open(filePathToWrite, "w") as fileToWrite: + fileToWrite.write(json.dumps(jsonData)) diff --git a/Web/Python/vtkmodules/web/render_window_serializer.py b/Web/Python/vtkmodules/web/render_window_serializer.py new file mode 100644 index 000000000..54318676a --- /dev/null +++ b/Web/Python/vtkmodules/web/render_window_serializer.py @@ -0,0 +1,1410 @@ +import io +import logging +import struct +import time +import zipfile + +from vtkmodules.web import ( + base64Encode, + hashDataArray, + getJSArrayType, + arrayTypesMapping, + getReferenceId, +) + +from vtkmodules.vtkCommonCore import vtkTypeUInt32Array +from vtkmodules.vtkFiltersGeometry import vtkCompositeDataGeometryFilter +from vtkmodules.vtkFiltersGeometry import vtkDataSetSurfaceFilter +from vtkmodules.vtkRenderingCore import vtkColorTransferFunction + +logger = logging.getLogger(__name__) +# Always DEBUG level for this logger. Users can change this +logger.setLevel(logging.DEBUG) + +# ----------------------------------------------------------------------------- +# Array helpers +# ----------------------------------------------------------------------------- + +def zipCompression(name, data): + with io.BytesIO() as in_memory: + with zipfile.ZipFile(in_memory, mode="w") as zf: + zf.writestr("data/%s" % name, data, zipfile.ZIP_DEFLATED) + in_memory.seek(0) + return in_memory.read() + + +def dataTableToList(dataTable): + dataType = arrayTypesMapping[dataTable.GetDataType()] + elementSize = struct.calcsize(dataType) + nbValues = dataTable.GetNumberOfValues() + nbComponents = dataTable.GetNumberOfComponents() + nbytes = elementSize * nbValues + if dataType != " ": + with io.BytesIO(memoryview(dataTable)) as stream: + data = list(struct.unpack(dataType * nbValues, stream.read(nbytes))) + return [ + data[idx * nbComponents : (idx + 1) * nbComponents] + for idx in range(nbValues // nbComponents) + ] + + return None + + +# ----------------------------------------------------------------------------- + + +def linspace(start, stop, num): + delta = (stop - start) / (num - 1) + return [start + i * delta for i in range(num)] + + +# ----------------------------------------------------------------------------- +# Convenience class for caching data arrays, storing computed sha sums, keeping +# track of valid actors, etc... +# ----------------------------------------------------------------------------- + + +class SynchronizationContext: + def __init__(self): + self.dataArrayCache = {} + self.lastDependenciesMapping = {} + self.ingoreLastDependencies = False + + def setIgnoreLastDependencies(self, force): + self.ingoreLastDependencies = force + + def cacheDataArray(self, pMd5, data): + self.dataArrayCache[pMd5] = data + + def getCachedDataArray(self, pMd5, binary=False, compression=False): + cacheObj = self.dataArrayCache[pMd5] + array = cacheObj["array"] + cacheTime = cacheObj["mTime"] + + if cacheTime != array.GetMTime(): + logger.debug(" ***** ERROR: you asked for an old cache key! ***** ") + + if array.GetDataType() == 12: + # IdType need to be converted to Uint32 + arraySize = array.GetNumberOfTuples() * array.GetNumberOfComponents() + newArray = vtkTypeUInt32Array() + newArray.SetNumberOfTuples(arraySize) + for i in range(arraySize): + newArray.SetValue(i, -1 if array.GetValue(i) < 0 else array.GetValue(i)) + pBuffer = memoryview(newArray) + else: + pBuffer = memoryview(array) + + if binary: + # Convert the vtkUnsignedCharArray into a bytes object, required by + # Autobahn websockets + return ( + pBuffer.tobytes() + if not compression + else zipCompression(pMd5, pBuffer.tobytes()) + ) + + return base64Encode( + pBuffer if not compression else zipCompression(pMd5, pBuffer.tobytes()) + ) + + def checkForArraysToRelease(self, timeWindow=20): + cutOffTime = time.time() - timeWindow + shasToDelete = [] + for sha in self.dataArrayCache: + record = self.dataArrayCache[sha] + array = record["array"] + count = array.GetReferenceCount() + + if count == 1 and record["ts"] < cutOffTime: + shasToDelete.append(sha) + + for sha in shasToDelete: + del self.dataArrayCache[sha] + + def getLastDependencyList(self, idstr): + lastDeps = [] + if idstr in self.lastDependenciesMapping and not self.ingoreLastDependencies: + lastDeps = self.lastDependenciesMapping[idstr] + return lastDeps + + def setNewDependencyList(self, idstr, depList): + self.lastDependenciesMapping[idstr] = depList + + def buildDependencyCallList(self, idstr, newList, addMethod, removeMethod): + oldList = self.getLastDependencyList(idstr) + + calls = [] + calls += [[addMethod, [wrapId(x)]] for x in newList if x not in oldList] + calls += [[removeMethod, [wrapId(x)]] for x in oldList if x not in newList] + + self.setNewDependencyList(idstr, newList) + return calls + + +# ----------------------------------------------------------------------------- +# Global variables +# ----------------------------------------------------------------------------- + +SERIALIZERS = {} +JS_CLASS_MAPPING = {} +context = None + +# ----------------------------------------------------------------------------- +# Global API +# ----------------------------------------------------------------------------- + + +def registerInstanceSerializer(name, method): + global SERIALIZERS + SERIALIZERS[name] = method + +def registerJSClass(vtk_class, js_class): + global JS_CLASS_MAPPING + JS_CLASS_MAPPING[vtk_class] = js_class + +def class_name(vtk_obj): + vtk_class = vtk_obj.GetClassName() + if vtk_class in JS_CLASS_MAPPING: + return JS_CLASS_MAPPING[vtk_class] + + return vtk_class + + +# ----------------------------------------------------------------------------- + + +def serializeInstance(parent, instance, instanceId, context, depth): + instanceType = class_name(instance) + serializer = SERIALIZERS[instanceType] if instanceType in SERIALIZERS else None + + if serializer: + return serializer(parent, instance, instanceId, context, depth) + + logger.error(f"!!!No serializer for {instanceType} with id {instanceId}") + + return None + + +# ----------------------------------------------------------------------------- + + +def initializeSerializers(): + # Actors/viewProps + registerInstanceSerializer("vtkActor", genericActorSerializer) + registerInstanceSerializer("vtkOpenGLActor", genericActorSerializer) + registerInstanceSerializer("vtkPVLODActor", genericActorSerializer) + + # Volume/viewProps + registerInstanceSerializer("vtkVolume", genericVolumeSerializer) + + # Mappers + registerInstanceSerializer("vtkMapper", genericMapperSerializer) + registerInstanceSerializer("vtkDataSetMapper", genericMapperSerializer) + registerInstanceSerializer("vtkPolyDataMapper", genericMapperSerializer) + registerInstanceSerializer("vtkImageDataMapper", genericMapperSerializer) + registerInstanceSerializer("vtkOpenGLPolyDataMapper", genericMapperSerializer) + registerInstanceSerializer("vtkCompositePolyDataMapper2", genericMapperSerializer) + registerJSClass("vtkPolyDataMapper", "vtkMapper") + registerJSClass("vtkDataSetMapper", "vtkMapper") + registerJSClass("vtkOpenGLPolyDataMapper", "vtkMapper") + registerJSClass("vtkCompositePolyDataMapper2", "vtkMapper") + + registerInstanceSerializer("vtkVolumeMapper", genericVolumeMapperSerializer) + registerInstanceSerializer("vtkFixedPointVolumeRayCastMapper", genericVolumeMapperSerializer) + registerJSClass("vtkFixedPointVolumeRayCastMapper", "vtkVolumeMapper") + + # LookupTables/TransferFunctions + registerInstanceSerializer("vtkLookupTable", lookupTableSerializer2) + registerInstanceSerializer( + "vtkPVDiscretizableColorTransferFunction", discretizableColorTransferFunctionSerializer + ) + registerInstanceSerializer( + "vtkColorTransferFunction", colorTransferFunctionSerializer + ) + registerInstanceSerializer("vtkPiecewiseFunction", pwfSerializer) + + # Textures + registerInstanceSerializer("vtkTexture", textureSerializer) + registerInstanceSerializer("vtkOpenGLTexture", textureSerializer) + + # Property + registerInstanceSerializer("vtkProperty", propertySerializer) + registerInstanceSerializer("vtkOpenGLProperty", propertySerializer) + + # VolumeProperty + registerInstanceSerializer("vtkVolumeProperty", volumePropertySerializer) + + # Datasets + registerInstanceSerializer("vtkPolyData", polydataSerializer) + registerInstanceSerializer("vtkImageData", imagedataSerializer) + registerInstanceSerializer("vtkUnstructuredGrid", mergeToPolydataSerializer) + registerInstanceSerializer("vtkMultiBlockDataSet", mergeToPolydataSerializer) + registerInstanceSerializer("vtkStructuredPoints", imagedataSerializer) + registerJSClass("vtkStructuredPoints", "vtkImageData") + + + # RenderWindows + registerInstanceSerializer("vtkRenderWindow", renderWindowSerializer) + registerInstanceSerializer("vtkCocoaRenderWindow", renderWindowSerializer) + registerInstanceSerializer("vtkXOpenGLRenderWindow", renderWindowSerializer) + registerInstanceSerializer("vtkWin32OpenGLRenderWindow", renderWindowSerializer) + registerInstanceSerializer("vtkEGLRenderWindow", renderWindowSerializer) + registerInstanceSerializer("vtkOpenVRRenderWindow", renderWindowSerializer) + registerInstanceSerializer("vtkOpenXRRenderWindow", renderWindowSerializer) + registerInstanceSerializer("vtkGenericOpenGLRenderWindow", renderWindowSerializer) + registerInstanceSerializer("vtkOSOpenGLRenderWindow", renderWindowSerializer) + registerInstanceSerializer("vtkOpenGLRenderWindow", renderWindowSerializer) + registerInstanceSerializer("vtkIOSRenderWindow", renderWindowSerializer) + registerInstanceSerializer("vtkExternalOpenGLRenderWindow", renderWindowSerializer) + registerInstanceSerializer("vtkOffscreenOpenGLRenderWindow", renderWindowSerializer) + + # Renderers + registerInstanceSerializer("vtkRenderer", rendererSerializer) + registerInstanceSerializer("vtkOpenGLRenderer", rendererSerializer) + + # Cameras + registerInstanceSerializer("vtkCamera", cameraSerializer) + registerInstanceSerializer("vtkOpenGLCamera", cameraSerializer) + + # Lights + registerInstanceSerializer("vtkLight", lightSerializer) + registerInstanceSerializer("vtkPVLight", lightSerializer) + registerInstanceSerializer("vtkOpenGLLight", lightSerializer) + + # Annotations (ScalarBar/CubeAxes + registerInstanceSerializer("vtkCubeAxesActor", cubeAxesSerializer) + registerInstanceSerializer("vtkScalarBarActor", scalarBarActorSerializer) + + +# ----------------------------------------------------------------------------- +# Helper functions +# ----------------------------------------------------------------------------- + + +def pad(depth): + padding = "" + for _ in range(depth): + padding += " " + return padding + + +# ----------------------------------------------------------------------------- + + +def wrapId(idStr): + return "instance:${%s}" % idStr + + +# ----------------------------------------------------------------------------- + +dataArrayShaMapping = {} + + +def digest(array): + objId = getReferenceId(array) + + record = None + if objId in dataArrayShaMapping: + record = dataArrayShaMapping[objId] + + if record and record["mtime"] == array.GetMTime(): + return record["sha"] + + record = {"sha": hashDataArray(array), "mtime": array.GetMTime()} + + dataArrayShaMapping[objId] = record + return record["sha"] + + +# ----------------------------------------------------------------------------- + + +def getRangeInfo(array, component): + r = array.GetRange(component) + compRange = {} + compRange["min"] = r[0] + compRange["max"] = r[1] + compRange["component"] = array.GetComponentName(component) + return compRange + + +# ----------------------------------------------------------------------------- + + +def getArrayDescription(array, context): + if not array: + return None + + pMd5 = digest(array) + context.cacheDataArray( + pMd5, {"array": array, "mTime": array.GetMTime(), "ts": time.time()} + ) + + root = {} + root["hash"] = pMd5 + root["vtkClass"] = "vtkDataArray" + root["name"] = array.GetName() + root["dataType"] = getJSArrayType(array) + root["numberOfComponents"] = array.GetNumberOfComponents() + root["size"] = array.GetNumberOfComponents() * array.GetNumberOfTuples() + root["ranges"] = [] + if root["numberOfComponents"] > 1: + for i in range(root["numberOfComponents"]): + root["ranges"].append(getRangeInfo(array, i)) + root["ranges"].append(getRangeInfo(array, -1)) + else: + root["ranges"].append(getRangeInfo(array, 0)) + + return root + + +# ----------------------------------------------------------------------------- + + +def extractRequiredFields( + extractedFields, parent, dataset, context, requestedFields=["Normals", "TCoords"] +): + arrays_to_export = set() + export_all = "*" in requestedFields + # Identify arrays to export + if not export_all: + # FIXME should evolve and support funky mapper which leverage many arrays + if parent and parent.IsA("vtkMapper"): + mapper = parent + scalarVisibility = mapper.GetScalarVisibility() + arrayAccessMode = mapper.GetArrayAccessMode() + colorArrayName = ( + mapper.GetArrayName() if arrayAccessMode == 1 else mapper.GetArrayId() + ) + # colorMode = mapper.GetColorMode() + scalarMode = mapper.GetScalarMode() + if scalarVisibility and scalarMode in (1, 3): + array_to_export = dataset.GetPointData().GetArray(colorArrayName) + if array_to_export is None: + array_to_export = dataset.GetPointData().GetScalars() + arrays_to_export.add(array_to_export) + if scalarVisibility and scalarMode in (2, 4): + array_to_export = dataset.GetCellData().GetArray(colorArrayName) + if array_to_export is None: + array_to_export = dataset.GetCellData().GetScalars() + arrays_to_export.add(array_to_export) + if scalarVisibility and scalarMode == 0: + array_to_export = dataset.GetPointData().GetScalars() + if array_to_export is None: + array_to_export = dataset.GetCellData().GetScalars() + arrays_to_export.add(array_to_export) + + if parent and parent.IsA("vtkTexture") and dataset.GetPointData().GetScalars(): + arrays_to_export.add(dataset.GetPointData().GetScalars()) + + arrays_to_export.update( + [ + getattr(dataset.GetPointData(), "Get" + requestedField, lambda: None)() + for requestedField in requestedFields + ] + ) + + # Browse all arrays + for location, field_data in [ + ("pointData", dataset.GetPointData()), + ("cellData", dataset.GetCellData()), + ]: + for array_index in range(field_data.GetNumberOfArrays()): + array = field_data.GetArray(array_index) + if export_all or array in arrays_to_export: + arrayMeta = getArrayDescription(array, context) + if arrayMeta: + arrayMeta["location"] = location + attribute = field_data.IsArrayAnAttribute(array_index) + arrayMeta["registration"] = ( + "set" + field_data.GetAttributeTypeAsString(attribute) + if attribute >= 0 + else "addArray" + ) + extractedFields.append(arrayMeta) + +# ----------------------------------------------------------------------------- +# Concrete instance serializers +# ----------------------------------------------------------------------------- + + +def genericActorSerializer(parent, actor, actorId, context, depth): + # This kind of actor has two "children" of interest, a property and a + # mapper + actorVisibility = actor.GetVisibility() + mapperInstance = None + propertyInstance = None + calls = [] + dependencies = [] + + if actorVisibility: + mapper = None + if not hasattr(actor, "GetMapper"): + logger.debug("This actor does not have a GetMapper method") + else: + mapper = actor.GetMapper() + + if mapper: + mapperId = getReferenceId(mapper) + mapperInstance = serializeInstance( + actor, mapper, mapperId, context, depth + 1 + ) + if mapperInstance: + dependencies.append(mapperInstance) + calls.append(["setMapper", [wrapId(mapperId)]]) + + prop = None + if hasattr(actor, "GetProperty"): + prop = actor.GetProperty() + else: + logger.debug("This actor does not have a GetProperty method") + + if prop: + propId = getReferenceId(prop) + propertyInstance = serializeInstance( + actor, prop, propId, context, depth + 1 + ) + if propertyInstance: + dependencies.append(propertyInstance) + calls.append(["setProperty", [wrapId(propId)]]) + + # Handle texture if any + texture = None + if hasattr(actor, "GetTexture"): + texture = actor.GetTexture() + else: + logger.debug("This actor does not have a GetTexture method") + + if texture: + textureId = getReferenceId(texture) + textureInstance = serializeInstance( + actor, texture, textureId, context, depth + 1 + ) + if textureInstance: + dependencies.append(textureInstance) + calls.append(["addTexture", [wrapId(textureId)]]) + + if actorVisibility == 0 or (mapperInstance and propertyInstance): + return { + "parent": getReferenceId(parent), + "id": actorId, + "type": class_name(actor), + "properties": { + # vtkProp + "visibility": actorVisibility, + "pickable": actor.GetPickable(), + "dragable": actor.GetDragable(), + "useBounds": actor.GetUseBounds(), + # vtkProp3D + "origin": actor.GetOrigin(), + "position": actor.GetPosition(), + "scale": actor.GetScale(), + # vtkActor + "forceOpaque": actor.GetForceOpaque(), + "forceTranslucent": actor.GetForceTranslucent(), + }, + "calls": calls, + "dependencies": dependencies, + } + + return None + + +# ----------------------------------------------------------------------------- + + +def genericVolumeSerializer(parent, actor, actorId, context, depth): + # This kind of actor has two "children" of interest, a property and a + # mapper + actorVisibility = actor.GetVisibility() + mapperInstance = None + propertyInstance = None + calls = [] + dependencies = [] + + if actorVisibility: + mapper = None + if not hasattr(actor, "GetMapper"): + logger.debug("This actor does not have a GetMapper method") + else: + mapper = actor.GetMapper() + + if mapper: + mapperId = getReferenceId(mapper) + mapperInstance = serializeInstance( + actor, mapper, mapperId, context, depth + 1 + ) + if mapperInstance: + dependencies.append(mapperInstance) + calls.append(["setMapper", [wrapId(mapperId)]]) + + prop = None + if hasattr(actor, "GetProperty"): + prop = actor.GetProperty() + else: + logger.debug("This actor does not have a GetProperty method") + + if prop: + propId = getReferenceId(prop) + propertyInstance = serializeInstance( + actor, prop, propId, context, depth + 1 + ) + if propertyInstance: + dependencies.append(propertyInstance) + calls.append(["setProperty", [wrapId(propId)]]) + + if actorVisibility == 0 or (mapperInstance and propertyInstance): + return { + "parent": getReferenceId(parent), + "id": actorId, + "type": class_name(actor), + "properties": { + # vtkProp + "visibility": actorVisibility, + "pickable": actor.GetPickable(), + "dragable": actor.GetDragable(), + "useBounds": actor.GetUseBounds(), + # vtkProp3D + "origin": actor.GetOrigin(), + "position": actor.GetPosition(), + "scale": actor.GetScale(), + }, + "calls": calls, + "dependencies": dependencies, + } + + return None + +# ----------------------------------------------------------------------------- + + +def textureSerializer(parent, texture, textureId, context, depth): + # This kind of mapper requires us to get 2 items: input data and lookup + # table + dataObject = None + dataObjectInstance = None + calls = [] + dependencies = [] + + if hasattr(texture, "GetInput"): + dataObject = texture.GetInput() + else: + logger.debug("This texture does not have GetInput method") + + if dataObject: + dataObjectId = "%s-texture" % textureId + dataObjectInstance = serializeInstance( + texture, dataObject, dataObjectId, context, depth + 1 + ) + if dataObjectInstance: + dependencies.append(dataObjectInstance) + calls.append(["setInputData", [wrapId(dataObjectId)]]) + + if dataObjectInstance: + return { + "parent": getReferenceId(parent), + "id": textureId, + "type": "vtkTexture", + "properties": { + "interpolate": texture.GetInterpolate(), + "repeat": texture.GetRepeat(), + "edgeClamp": texture.GetEdgeClamp(), + }, + "calls": calls, + "dependencies": dependencies, + } + + return None + + +# ----------------------------------------------------------------------------- + + +def genericMapperSerializer(parent, mapper, mapperId, context, depth): + # This kind of mapper requires us to get 2 items: input data and lookup + # table + dataObject = None + dataObjectInstance = None + lookupTableInstance = None + calls = [] + dependencies = [] + + if hasattr(mapper, "GetInputDataObject"): + mapper.GetInputAlgorithm().Update() + dataObject = mapper.GetInputDataObject(0, 0) + else: + logger.debug("This mapper does not have GetInputDataObject method") + + if dataObject: + if dataObject.IsA("vtkDataSet"): + alg = vtkDataSetSurfaceFilter() + alg.SetInputData(dataObject) + alg.Update() + dataObject = alg.GetOutput() + + dataObjectId = "%s-dataset" % mapperId + dataObjectInstance = serializeInstance( + mapper, dataObject, dataObjectId, context, depth + 1 + ) + + if dataObjectInstance: + dependencies.append(dataObjectInstance) + calls.append(["setInputData", [wrapId(dataObjectId)]]) + + lookupTable = None + + if hasattr(mapper, "GetLookupTable"): + lookupTable = mapper.GetLookupTable() + else: + logger.debug("This mapper does not have GetLookupTable method") + + if lookupTable: + lookupTableId = getReferenceId(lookupTable) + lookupTableInstance = serializeInstance( + mapper, lookupTable, lookupTableId, context, depth + 1 + ) + if lookupTableInstance: + dependencies.append(lookupTableInstance) + calls.append( + ["setLookupTable", [wrapId(lookupTableId)]] + ) + + if dataObjectInstance: + colorArrayName = ( + mapper.GetArrayName() + if mapper.GetArrayAccessMode() == 1 + else mapper.GetArrayId() + ) + return { + "parent": getReferenceId(parent), + "id": mapperId, + "type": class_name(mapper), + "properties": { + "resolveCoincidentTopology": mapper.GetResolveCoincidentTopology(), + "renderTime": mapper.GetRenderTime(), + "arrayAccessMode": mapper.GetArrayAccessMode(), + "scalarRange": mapper.GetScalarRange(), + "useLookupTableScalarRange": 1 + if mapper.GetUseLookupTableScalarRange() + else 0, + "scalarVisibility": mapper.GetScalarVisibility(), + "colorByArrayName": colorArrayName, + "colorMode": mapper.GetColorMode(), + "scalarMode": mapper.GetScalarMode(), + "interpolateScalarsBeforeMapping": 1 + if mapper.GetInterpolateScalarsBeforeMapping() + else 0, + }, + "calls": calls, + "dependencies": dependencies, + } + + return None + + +# ----------------------------------------------------------------------------- + + +def genericVolumeMapperSerializer(parent, mapper, mapperId, context, depth): + # This kind of mapper requires us to get 2 items: input data and lookup + # table + dataObject = None + dataObjectInstance = None + lookupTableInstance = None + calls = [] + dependencies = [] + + if hasattr(mapper, "GetInputDataObject"): + mapper.GetInputAlgorithm().Update() + dataObject = mapper.GetInputDataObject(0, 0) + else: + logger.debug("This mapper does not have GetInputDataObject method") + + if dataObject: + dataObjectId = "%s-dataset" % mapperId + dataObjectInstance = serializeInstance( + mapper, dataObject, dataObjectId, context, depth + 1 + ) + + if dataObjectInstance: + dependencies.append(dataObjectInstance) + calls.append(["setInputData", [wrapId(dataObjectId)]]) + + if dataObjectInstance: + return { + "parent": getReferenceId(parent), + "id": mapperId, + "type": class_name(mapper), + "properties": { + # VolumeMapper + "sampleDistance": mapper.GetSampleDistance(), + "imageSampleDistance": mapper.GetImageSampleDistance(), + # "maximumSamplesPerRay": mapper.GetMaximumSamplesPerRay(), + "autoAdjustSampleDistances": mapper.GetAutoAdjustSampleDistances(), + "blendMode": mapper.GetBlendMode(), + # "ipScalarRange": mapper.GetIpScalarRange(), + # "filterMode": mapper.GetFilterMode(), + # "preferSizeOverAccuracy": mapper.Get(), + }, + "calls": calls, + "dependencies": dependencies, + } + + return None + +# ----------------------------------------------------------------------------- + + +def lookupTableSerializer(parent, lookupTable, lookupTableId, context, depth): + # No children in this case, so no additions to bindings and return empty list + # But we do need to add instance + + lookupTableRange = lookupTable.GetRange() + + lookupTableHueRange = [0.5, 0] + if hasattr(lookupTable, "GetHueRange"): + try: + lookupTable.GetHueRange(lookupTableHueRange) + except Exception as inst: + pass + + lutSatRange = lookupTable.GetSaturationRange() + lutAlphaRange = lookupTable.GetAlphaRange() + + return { + "parent": getReferenceId(parent), + "id": lookupTableId, + "type": class_name(lookupTable), + "properties": { + "numberOfColors": lookupTable.GetNumberOfColors(), + "valueRange": lookupTableRange, + "hueRange": lookupTableHueRange, + # 'alphaRange': lutAlphaRange, # Causes weird rendering artifacts on client + "saturationRange": lutSatRange, + "nanColor": lookupTable.GetNanColor(), + "belowRangeColor": lookupTable.GetBelowRangeColor(), + "aboveRangeColor": lookupTable.GetAboveRangeColor(), + "useAboveRangeColor": True + if lookupTable.GetUseAboveRangeColor() + else False, + "useBelowRangeColor": True + if lookupTable.GetUseBelowRangeColor() + else False, + "alpha": lookupTable.GetAlpha(), + "vectorSize": lookupTable.GetVectorSize(), + "vectorComponent": lookupTable.GetVectorComponent(), + "vectorMode": lookupTable.GetVectorMode(), + "indexedLookup": lookupTable.GetIndexedLookup(), + }, + } + + +# ----------------------------------------------------------------------------- + + +def lookupTableToColorTransferFunction(lookupTable): + dataTable = lookupTable.GetTable() + table = dataTableToList(dataTable) + if table: + ctf = vtkColorTransferFunction() + tableRange = lookupTable.GetTableRange() + points = linspace(*tableRange, num=len(table)) + for x, rgba in zip(points, table): + ctf.AddRGBPoint(x, *[x / 255 for x in rgba[:3]]) + + return ctf + + return None + + +def lookupTableSerializer2(parent, lookupTable, lookupTableId, context, depth): + ctf = lookupTableToColorTransferFunction(lookupTable) + if ctf: + return colorTransferFunctionSerializer( + parent, ctf, lookupTableId, context, depth + ) + + return None + + +# ----------------------------------------------------------------------------- + + +def propertySerializer(parent, propObj, propObjId, context, depth): + representation = ( + propObj.GetRepresentation() if hasattr(propObj, "GetRepresentation") else 2 + ) + colorToUse = ( + propObj.GetDiffuseColor() if hasattr(propObj, "GetDiffuseColor") else [1, 1, 1] + ) + if representation == 1 and hasattr(propObj, "GetColor"): + colorToUse = propObj.GetColor() + + return { + "parent": getReferenceId(parent), + "id": propObjId, + "type": class_name(propObj), + "properties": { + "representation": representation, + "diffuseColor": colorToUse, + "color": propObj.GetColor(), + "ambientColor": propObj.GetAmbientColor(), + "specularColor": propObj.GetSpecularColor(), + "edgeColor": propObj.GetEdgeColor(), + "ambient": propObj.GetAmbient(), + "diffuse": propObj.GetDiffuse(), + "specular": propObj.GetSpecular(), + "specularPower": propObj.GetSpecularPower(), + "opacity": propObj.GetOpacity(), + "interpolation": propObj.GetInterpolation(), + "edgeVisibility": 1 if propObj.GetEdgeVisibility() else 0, + "backfaceCulling": 1 if propObj.GetBackfaceCulling() else 0, + "frontfaceCulling": 1 if propObj.GetFrontfaceCulling() else 0, + "pointSize": propObj.GetPointSize(), + "lineWidth": propObj.GetLineWidth(), + "lighting": 1 if propObj.GetLighting() else 0, + }, + } + +def volumePropertySerializer(parent, propObj, propObjId, context, depth): + calls = [] + dependencies = [] + + # Color handling + lut = propObj.GetRGBTransferFunction() + if lut: + lookupTableId = getReferenceId(lut) + lookupTableInstance = serializeInstance( + propObj, lut, lookupTableId, context, depth + 1 + ) + + if lookupTableInstance: + dependencies.append(lookupTableInstance) + calls.append(["setRGBTransferFunction", [0, wrapId(lookupTableId)]]) + + # Piecewise handling + pwf = propObj.GetScalarOpacity() + if pwf: + pwfId = getReferenceId(pwf) + pwfInstance = serializeInstance( + propObj, pwf, pwfId, context, depth + 1 + ) + + if pwfInstance: + dependencies.append(pwfInstance) + calls.append(["setScalarOpacity", [0, wrapId(pwfId)]]) + + return { + "parent": getReferenceId(parent), + "id": propObjId, + "type": class_name(propObj), + "properties": { + "independentComponents": propObj.GetIndependentComponents(), + "interpolationType": propObj.GetInterpolationType(), + "shade": propObj.GetShade(), + "ambient": propObj.GetAmbient(), + "diffuse": propObj.GetDiffuse(), + "specular": propObj.GetSpecular(), + "specularPower": propObj.GetSpecularPower(), + # "useLabelOutline": propObj.GetUseLabelOutline(), + # "labelOutlineThickness": propObj.GetLabelOutlineThickness(), + }, + "calls": calls, + "dependencies": dependencies, + } + +# ----------------------------------------------------------------------------- + + +def imagedataSerializer(parent, dataset, datasetId, context, depth, requested_fields = ["Normals", "TCoords"]): + if hasattr(dataset, "GetDirectionMatrix"): + direction = [dataset.GetDirectionMatrix().GetElement(0, i) for i in range(9)] + else: + direction = [1, 0, 0, 0, 1, 0, 0, 0, 1] + + # Extract dataset fields + fields = [] + extractRequiredFields(fields, parent, dataset, context, "*") + + return { + "parent": getReferenceId(parent), + "id": datasetId, + "type": class_name(dataset), + "properties": { + "spacing": dataset.GetSpacing(), + "origin": dataset.GetOrigin(), + "dimensions": dataset.GetDimensions(), + "direction": direction, + "fields": fields, + }, + } + + +# ----------------------------------------------------------------------------- + + +def polydataSerializer(parent, dataset, datasetId, context, depth, requested_fields = ["Normals", "TCoords"]): + if dataset and dataset.GetPoints(): + properties = {} + + # Points + points = getArrayDescription(dataset.GetPoints().GetData(), context) + points["vtkClass"] = "vtkPoints" + properties["points"] = points + + # Verts + if dataset.GetVerts() and dataset.GetVerts().GetData().GetNumberOfTuples() > 0: + _verts = getArrayDescription(dataset.GetVerts().GetData(), context) + properties["verts"] = _verts + properties["verts"]["vtkClass"] = "vtkCellArray" + + # Lines + if dataset.GetLines() and dataset.GetLines().GetData().GetNumberOfTuples() > 0: + _lines = getArrayDescription(dataset.GetLines().GetData(), context) + properties["lines"] = _lines + properties["lines"]["vtkClass"] = "vtkCellArray" + + # Polys + if dataset.GetPolys() and dataset.GetPolys().GetData().GetNumberOfTuples() > 0: + _polys = getArrayDescription(dataset.GetPolys().GetData(), context) + properties["polys"] = _polys + properties["polys"]["vtkClass"] = "vtkCellArray" + + # Strips + if ( + dataset.GetStrips() + and dataset.GetStrips().GetData().GetNumberOfTuples() > 0 + ): + _strips = getArrayDescription(dataset.GetStrips().GetData(), context) + properties["strips"] = _strips + properties["strips"]["vtkClass"] = "vtkCellArray" + + # Fields + properties["fields"] = [] + extractRequiredFields(properties["fields"], parent, dataset, context, requested_fields) + + return { + "parent": getReferenceId(parent), + "id": datasetId, + "type": class_name(dataset), + "properties": properties, + } + + logger.debug("This dataset has no points!") + return None + + +# ----------------------------------------------------------------------------- + + +def mergeToPolydataSerializer(parent, dataObject, dataObjectId, context, depth, requested_fields=["Normals", "TCoords"]): + dataset = None + + if dataObject.IsA("vtkCompositeDataSet"): + gf = vtkCompositeDataGeometryFilter() + gf.SetInputData(dataObject) + gf.Update() + dataset = gf.GetOutput() + elif dataObject.IsA("vtkUnstructuredGrid"): + gf = vtkDataSetSurfaceFilter() + gf.SetInputData(dataObject) + gf.Update() + dataset = gf.GetOutput() + else: + dataset = mapper.GetInput() + + return polydataSerializer(parent, dataset, dataObjectId, context, depth, requested_fields) + + +# ----------------------------------------------------------------------------- + + +def colorTransferFunctionSerializer(parent, instance, objId, context, depth): + nodes = [] + + for i in range(instance.GetSize()): + # x, r, g, b, midpoint, sharpness + node = [0, 0, 0, 0, 0, 0] + instance.GetNodeValue(i, node) + nodes.append(node) + + return { + "parent": getReferenceId(parent), + "id": objId, + "type": class_name(instance), + "properties": { + "clamping": 1 if instance.GetClamping() else 0, + "colorSpace": instance.GetColorSpace(), + "hSVWrap": 1 if instance.GetHSVWrap() else 0, + # 'nanColor': instance.GetNanColor(), # Breaks client + # 'belowRangeColor': instance.GetBelowRangeColor(), # Breaks client + # 'aboveRangeColor': instance.GetAboveRangeColor(), # Breaks client + # 'useAboveRangeColor': 1 if instance.GetUseAboveRangeColor() else 0, + # 'useBelowRangeColor': 1 if instance.GetUseBelowRangeColor() else 0, + "allowDuplicateScalars": 1 if instance.GetAllowDuplicateScalars() else 0, + "alpha": instance.GetAlpha(), + "vectorComponent": instance.GetVectorComponent(), + "vectorSize": instance.GetVectorSize(), + "vectorMode": instance.GetVectorMode(), + "indexedLookup": instance.GetIndexedLookup(), + "nodes": nodes, + }, + } + +def discretizableColorTransferFunctionSerializer(parent, instance, objId, context, depth): + ctf = colorTransferFunctionSerializer(parent, instance, objId, context, depth) + ctf["properties"]["discretize"] = instance.GetDiscretize() + ctf["properties"]["numberOfValues"] = instance.GetNumberOfValues() + return ctf + +# ----------------------------------------------------------------------------- + +def pwfSerializer(parent, instance, objId, context, depth): + nodes = [] + + for i in range(instance.GetSize()): + # x, y, midpoint, sharpness + node = [0, 0, 0, 0] + instance.GetNodeValue(i, node) + nodes.append(node) + + return { + "parent": getReferenceId(parent), + "id": objId, + "type": class_name(instance), + "properties": { + "range": list(instance.GetRange()), + "clamping": instance.GetClamping(), + "allowDuplicateScalars": instance.GetAllowDuplicateScalars(), + "nodes": nodes, + }, + } + +# ----------------------------------------------------------------------------- + +def cubeAxesSerializer(parent, actor, actorId, context, depth): + """ + Possible add-on properties for vtk.js: + gridLines: True, + axisLabels: None, + axisTitlePixelOffset: 35.0, + axisTextStyle: { + fontColor: 'white', + fontStyle: 'normal', + fontSize: 18, + fontFamily: 'serif', + }, + tickLabelPixelOffset: 12.0, + tickTextStyle: { + fontColor: 'white', + fontStyle: 'normal', + fontSize: 14, + fontFamily: 'serif', + }, + """ + axisLabels = ["", "", ""] + if actor.GetXAxisLabelVisibility(): + axisLabels[0] = actor.GetXTitle() + if actor.GetYAxisLabelVisibility(): + axisLabels[1] = actor.GetYTitle() + if actor.GetZAxisLabelVisibility(): + axisLabels[2] = actor.GetZTitle() + + return { + "parent": getReferenceId(parent), + "id": actorId, + "type": "vtkCubeAxesActor", + "properties": { + # vtkProp + "visibility": actor.GetVisibility(), + "pickable": actor.GetPickable(), + "dragable": actor.GetDragable(), + "useBounds": actor.GetUseBounds(), + # vtkProp3D + "origin": actor.GetOrigin(), + "position": actor.GetPosition(), + "scale": actor.GetScale(), + # vtkActor + "forceOpaque": actor.GetForceOpaque(), + "forceTranslucent": actor.GetForceTranslucent(), + # vtkCubeAxesActor + "dataBounds": actor.GetBounds(), + "faceVisibilityAngle": 8, + "gridLines": True, + "axisLabels": axisLabels, + "axisTitlePixelOffset": 35.0, + "axisTextStyle": { + "fontColor": "white", + "fontStyle": "normal", + "fontSize": 18, + "fontFamily": "serif", + }, + "tickLabelPixelOffset": 12.0, + "tickTextStyle": { + "fontColor": "white", + "fontStyle": "normal", + "fontSize": 14, + "fontFamily": "serif", + }, + }, + "calls": [["setCamera", [wrapId(getReferenceId(actor.GetCamera()))]]], + "dependencies": [], + } + +# ----------------------------------------------------------------------------- + +def scalarBarActorSerializer(parent, actor, actorId, context, depth): + dependencies = [] + calls = [] + lut = actor.GetLookupTable() + if not lut: + return None + + lutId = getReferenceId(lut) + lutInstance = serializeInstance(actor, lut, lutId, context, depth + 1) + if not lutInstance: + return None + + dependencies.append(lutInstance) + calls.append(["setScalarsToColors", [wrapId(lutId)]]) + + prop = None + if hasattr(actor, "GetProperty"): + prop = actor.GetProperty() + else: + logger.debug("This scalarBarActor does not have a GetProperty method") + + if prop: + propId = getReferenceId(prop) + propertyInstance = serializeInstance( + actor, prop, propId, context, depth + 1 + ) + if propertyInstance: + dependencies.append(propertyInstance) + calls.append(["setProperty", [wrapId(propId)]]) + + axisLabel = actor.GetTitle() + width = actor.GetWidth() + height = actor.GetHeight() + + return { + "parent": getReferenceId(parent), + "id": actorId, + "type": "vtkScalarBarActor", + "properties": { + # vtkProp + "visibility": actor.GetVisibility(), + "pickable": actor.GetPickable(), + "dragable": actor.GetDragable(), + "useBounds": actor.GetUseBounds(), + # vtkActor2D + # "position": actor.GetPosition(), + # "position2": actor.GetPosition2(), + # "width": actor.GetWidth(), + # "height": actor.GetHeight(), + # vtkScalarBarActor + "automated": True, + "axisLabel": axisLabel, + # 'barPosition': [0, 0], + # 'barSize': [0, 0], + "boxPosition": [0.88, -0.92], + "boxSize": [width, height], + "axisTitlePixelOffset": 36.0, + "axisTextStyle": { + "fontColor": actor.GetTitleTextProperty().GetColor(), + "fontStyle": "normal", + "fontSize": 18, + "fontFamily": "serif", + }, + "tickLabelPixelOffset": 14.0, + "tickTextStyle": { + "fontColor": actor.GetTitleTextProperty().GetColor(), + "fontStyle": "normal", + "fontSize": 14, + "fontFamily": "serif", + }, + "drawNanAnnotation": actor.GetDrawNanAnnotation(), + "drawBelowRangeSwatch": actor.GetDrawBelowRangeSwatch(), + "drawAboveRangeSwatch": actor.GetDrawAboveRangeSwatch(), + }, + "calls": calls, + "dependencies": dependencies, + } + +# ----------------------------------------------------------------------------- + + +def rendererSerializer(parent, instance, objId, context, depth): + dependencies = [] + viewPropIds = [] + lightsIds = [] + calls = [] + + # Camera + camera = instance.GetActiveCamera() + cameraId = getReferenceId(camera) + cameraInstance = serializeInstance(instance, camera, cameraId, context, depth + 1) + if cameraInstance: + dependencies.append(cameraInstance) + calls.append(["setActiveCamera", [wrapId(cameraId)]]) + + # View prop as representation containers + viewPropCollection = instance.GetViewProps() + for rpIdx in range(viewPropCollection.GetNumberOfItems()): + viewProp = viewPropCollection.GetItemAsObject(rpIdx) + viewPropId = getReferenceId(viewProp) + + viewPropInstance = serializeInstance( + instance, viewProp, viewPropId, context, depth + 1 + ) + if viewPropInstance: + dependencies.append(viewPropInstance) + viewPropIds.append(viewPropId) + + calls += context.buildDependencyCallList( + "%s-props" % objId, viewPropIds, "addViewProp", "removeViewProp" + ) + + # Lights + lightCollection = instance.GetLights() + for lightIdx in range(lightCollection.GetNumberOfItems()): + light = lightCollection.GetItemAsObject(lightIdx) + lightId = getReferenceId(light) + + lightInstance = serializeInstance(instance, light, lightId, context, depth + 1) + if lightInstance: + dependencies.append(lightInstance) + lightsIds.append(lightId) + + calls += context.buildDependencyCallList( + "%s-lights" % objId, lightsIds, "addLight", "removeLight" + ) + + if len(dependencies) > 1: + return { + "parent": getReferenceId(parent), + "id": objId, + "type": class_name(instance), + "properties": { + "background": instance.GetBackground(), + "background2": instance.GetBackground2(), + "viewport": instance.GetViewport(), + # These commented properties do not yet have real setters in vtk.js + # 'gradientBackground': instance.GetGradientBackground(), + # 'aspect': instance.GetAspect(), + # 'pixelAspect': instance.GetPixelAspect(), + # 'ambient': instance.GetAmbient(), + "twoSidedLighting": instance.GetTwoSidedLighting(), + "lightFollowCamera": instance.GetLightFollowCamera(), + "layer": instance.GetLayer(), + "preserveColorBuffer": instance.GetPreserveColorBuffer(), + "preserveDepthBuffer": instance.GetPreserveDepthBuffer(), + "nearClippingPlaneTolerance": instance.GetNearClippingPlaneTolerance(), + "clippingRangeExpansion": instance.GetClippingRangeExpansion(), + "useShadows": instance.GetUseShadows(), + "useDepthPeeling": instance.GetUseDepthPeeling(), + "occlusionRatio": instance.GetOcclusionRatio(), + "maximumNumberOfPeels": instance.GetMaximumNumberOfPeels(), + "interactive": instance.GetInteractive(), + }, + "dependencies": dependencies, + "calls": calls, + } + + return None + + +# ----------------------------------------------------------------------------- + + +def cameraSerializer(parent, instance, objId, context, depth): + return { + "parent": getReferenceId(parent), + "id": objId, + "type": class_name(instance), + "properties": { + "focalPoint": instance.GetFocalPoint(), + "position": instance.GetPosition(), + "viewUp": instance.GetViewUp(), + "clippingRange": instance.GetClippingRange(), + }, + } + + +# ----------------------------------------------------------------------------- + + +def lightTypeToString(value): + """ + #define VTK_LIGHT_TYPE_HEADLIGHT 1 + #define VTK_LIGHT_TYPE_CAMERA_LIGHT 2 + #define VTK_LIGHT_TYPE_SCENE_LIGHT 3 + + 'HeadLight'; + 'SceneLight'; + 'CameraLight' + """ + if value == 1: + return "HeadLight" + elif value == 2: + return "CameraLight" + + return "SceneLight" + + +def lightSerializer(parent, instance, objId, context, depth): + return { + "parent": getReferenceId(parent), + "id": objId, + "type": class_name(instance), + "properties": { + # 'specularColor': instance.GetSpecularColor(), + # 'ambientColor': instance.GetAmbientColor(), + "switch": instance.GetSwitch(), + "intensity": instance.GetIntensity(), + "color": instance.GetDiffuseColor(), + "position": instance.GetPosition(), + "focalPoint": instance.GetFocalPoint(), + "positional": instance.GetPositional(), + "exponent": instance.GetExponent(), + "coneAngle": instance.GetConeAngle(), + "attenuationValues": instance.GetAttenuationValues(), + "lightType": lightTypeToString(instance.GetLightType()), + "shadowAttenuation": instance.GetShadowAttenuation(), + }, + } + + +# ----------------------------------------------------------------------------- + + +def renderWindowSerializer(parent, instance, objId, context, depth): + dependencies = [] + rendererIds = [] + + rendererCollection = instance.GetRenderers() + for rIdx in range(rendererCollection.GetNumberOfItems()): + # Grab the next vtkRenderer + renderer = rendererCollection.GetItemAsObject(rIdx) + rendererId = getReferenceId(renderer) + rendererInstance = serializeInstance( + instance, renderer, rendererId, context, depth + 1 + ) + if rendererInstance: + dependencies.append(rendererInstance) + rendererIds.append(rendererId) + + calls = context.buildDependencyCallList( + objId, rendererIds, "addRenderer", "removeRenderer" + ) + + return { + "parent": getReferenceId(parent), + "id": objId, + "type": class_name(instance), + "properties": {"numberOfLayers": instance.GetNumberOfLayers()}, + "dependencies": dependencies, + "calls": calls, + "mtime": instance.GetMTime(), + } diff --git a/Web/Python/vtkmodules/web/testing.py b/Web/Python/vtkmodules/web/testing.py new file mode 100644 index 000000000..8b74de5b7 --- /dev/null +++ b/Web/Python/vtkmodules/web/testing.py @@ -0,0 +1,788 @@ +r""" + This module provides some testing functionality for paraview and + vtk web applications. It provides the ability to run an arbitrary + test script in a separate thread and communicate the results back + to the service so that the CTest framework can be notified of the + success or failure of the test. + + This test harness will notice when the test script has finished + running and will notify the service to stop. At this point, the + test results will be checked in the main thread which ran the + service, and in the case of failure an exception will be raised + to notify CTest of the failure. + + Test scripts need to follow some simple rules in order to work + within the test harness framework: + + 1) implement a function called "runTest(args)", where the args + parameter contains all the arguments given to the web application + upon starting. Among other important items, args will contain the + port number where the web application is listening. + + 2) import the testing module so that the script has access to + the functions which indicate success and failure. Also the + testing module contains convenience functions that might be of + use to the test scripts. + + from vtk.web import testing + + 3) Call the "testPass(testName)" or "testFail(testName)" functions + from within the runTest(args) function to indicate to the framework + whether the test passed or failed. + +""" + +import_warning_info = "" +test_module_comm_queue = None + +from vtkmodules.vtkTestingRendering import vtkTesting + +# Try standard Python imports +try: + import os, re, time, datetime, threading, imp, inspect, Queue, types, io +except: + import_warning_info += "\nUnable to load at least one basic Python module" + +# Image comparison imports +try: + try: + from PIL import Image + except ImportError: + import Image + except: + raise + import base64 + import itertools +except: + import_warning_info += ( + "\nUnable to load at least one modules necessary for image comparison" + ) + +# Browser testing imports +try: + import selenium + from selenium import webdriver +except: + import_warning_info += ( + "\nUnable to load at least one module necessary for browser tests" + ) + +# HTTP imports +try: + import requests +except: + import_warning_info += ( + "\nUnable to load at least one module necessary for HTTP tests" + ) + + +# Define some infrastructure to support different (or no) browsers +test_module_browsers = ["firefox", "chrome", "internet_explorer", "safari", "nobrowser"] + + +class TestModuleBrowsers: + firefox, chrome, internet_explorer, safari, nobrowser = range(5) + + +# ============================================================================= +# We can use this exception type to indicate that the test shouldn't actually +# "fail", rather that it was unable to run because some dependencies were not +# met. +# ============================================================================= +class DependencyError(Exception): + def __init__(self, value): + self.value = value + + def __str__(self): + return repr(self.value) + + +# ============================================================================= +# This class allows usage as a dictionary and an object with named property +# access. +# ============================================================================= +class Dictionary(dict): + def __getattribute__(self, attrName): + return self[attrName] + + def __setattr__(self, attrName, attrValue): + self[attrName] = attrValue + + +# ============================================================================= +# Checks whether test script supplied, if so, safely imports needed modules +# ============================================================================= +def initialize(opts, reactor=None, cleanupMethod=None): + """ + This function should be called to initialize the testing module. The first + important thing it does is to store the options for later, since the + startTestThread function will need them. Then it checks the arguments that + were passed into the server to see if a test was actually requested, making + a note of this fact. Then, if a test was required, this function then + checks if all the necessary testing modules were safely imported, printing + a warning if not. If tests were requested and all modules were present, + then this function sets "test_module_do_testing" to True and sets up the + startTestThread function to be called after the reactor is running. + + opts: Parsed arguments from the server + + reactor: This argument is optional, but is used by server.py to + cause the test thread to be started only after the server itself + has started. If it is not provided, the test thread is launched + immediately. + + cleanupMethod: A callback method you would like the test thread + to execute when the test has finished. This is used by server.py + as a way to have the server terminated after the test has finished, + but could be used for other cleanup purposes. This argument is + also optional. + """ + + global import_warning_info + + global testModuleOptions + testModuleOptions = Dictionary() + + # Copy the testing options into something we can easily extend + for arg in vars(opts): + optValue = getattr(opts, arg) + testModuleOptions[arg] = optValue + + # If we got one, add the cleanup method to the testing options + if cleanupMethod: + testModuleOptions["cleanupMethod"] = cleanupMethod + + # Check if a test was actually requested + if ( + testModuleOptions.testScriptPath != "" + and testModuleOptions.testScriptPath is not None + ): + # Check if we ran into trouble with any of the testing imports + if import_warning_info != "": + print("WARNING: Some tests may have unmet dependencies") + print(import_warning_info) + + if reactor is not None: + # Add startTest callback to the reactor callback queue, so that + # the test thread gets started after the reactor is running. Of + # course this should only happen if everything is good for tests. + reactor.callWhenRunning(_start_test_thread) + else: + # Otherwise, our aim is to start the thread from another process + # so just call the start method. + _start_test_thread() + + +# ============================================================================= +# Grab out the command-line arguments needed for by the testing module. +# ============================================================================= +def add_arguments(parser): + """ + This function retrieves any command-line arguments that the client-side + tester needs. In order to run a test, you will typically just need the + following: + + --run-test-script => This should be the full path to the test script to + be run. + + --baseline-img-dir => This should be the 'Baseline' directory where the + baseline images for this test are located. + + --test-use-browser => This should be one of the supported browser types, + or else 'nobrowser'. The choices are 'chrome', 'firefox', 'internet_explorer', + 'safari', or 'nobrowser'. + """ + + parser.add_argument( + "--run-test-script", + default="", + help="The path to a test script to run", + dest="testScriptPath", + ) + + parser.add_argument( + "--baseline-img-dir", + default="", + help="The path to the directory containing the web test baseline images", + dest="baselineImgDir", + ) + + parser.add_argument( + "--test-use-browser", + default="nobrowser", + help="One of 'chrome', 'firefox', 'internet_explorer', 'safari', or 'nobrowser'.", + dest="useBrowser", + ) + + parser.add_argument( + "--temporary-directory", + default=".", + help="A temporary directory for storing test images and diffs", + dest="tmpDirectory", + ) + + parser.add_argument( + "--test-image-file-name", + default="", + help="Name of file in which to store generated test image", + dest="testImgFile", + ) + + +# ============================================================================= +# Initialize the test client +# ============================================================================= +def _start_test_thread(): + """ + This function checks whether testing is required and if so, sets up a Queue + for the purpose of communicating with the thread. then it starts the + after waiting 5 seconds for the server to have a chance to start up. + """ + + global test_module_comm_queue + test_module_comm_queue = Queue.Queue() + + t = threading.Thread( + target=launch_web_test, + args=[], + kwargs={ + "serverOpts": testModuleOptions, + "commQueue": test_module_comm_queue, + "testScript": testModuleOptions.testScriptPath, + }, + ) + + t.start() + + +# ============================================================================= +# Test scripts call this function to indicate passage of their test +# ============================================================================= +def test_pass(testName): + """ + Test scripts should call this function to indicate that the test passed. A + note is recorded that the test succeeded, and is checked later on from the + main thread so that CTest can be notified of this result. + """ + + global test_module_comm_queue + resultObj = {testName: "pass"} + test_module_comm_queue.put(resultObj) + + +# ============================================================================= +# Test scripts call this function to indicate failure of their test +# ============================================================================= +def test_fail(testName): + """ + Test scripts should call this function to indicate that the test failed. A + note is recorded that the test did not succeed, and this note is checked + later from the main thread so that CTest can be notified of the result. + + The main thread is the only one that can signal test failure in + CTest framework, and the main thread won't have a chance to check for + passage or failure of the test until the main loop has terminated. So + here we just record the failure result, then we check this result in the + processTestResults() function, throwing an exception at that point to + indicate to CTest that the test failed. + """ + + global test_module_comm_queue + resultObj = {testName: "fail"} + test_module_comm_queue.put(resultObj) + + +# ============================================================================= +# Concatenate any number of strings into a single path string. +# ============================================================================= +def concat_paths(*pathElts): + """ + A very simple convenience function so that test scripts can build platform + independent paths out of a list of elements, without having to import the + os module. + + pathElts: Any number of strings which should be concatenated together + in a platform independent manner. + """ + + return os.path.join(*pathElts) + + +# ============================================================================= +# So we can change our time format in a single place, this function is +# provided. +# ============================================================================= +def get_current_time_string(): + """ + This function returns the current time as a string, using ISO 8601 format. + """ + + return datetime.datetime.now().isoformat(" ") + + +# ============================================================================= +# Uses vtkTesting to compare images. According to comments in the vtkTesting +# C++ code (and this seems to work), if there are multiple baseline images in +# the same directory as the baseline_img, and they follow the naming pattern: +# 'img.png', 'img_1.png', ... , 'img_N.png', then all of these images will be +# tried for a match. +# ============================================================================= +def compare_images(test_img, baseline_img, tmp_dir="."): + """ + This function creates a vtkTesting object, and specifies the name of the + baseline image file, using a fully qualified path (baseline_img must be + fully qualified). Then it calls the vtkTesting method which compares the + image (test_img, specified only with a relative path) against the baseline + image as well as any other images in the same directory as the baseline + image which follow the naming pattern: 'img.png', 'img_1.png', ... , 'img_N.png' + + test_img: File name of output image to be compared against baseline. + + baseline_img: Fully qualified path to first of the baseline images. + + tmp_dir: Fully qualified path to a temporary directory for storing images. + """ + + # Create a vtkTesting object and specify a baseline image + t = vtkTesting() + t.AddArgument("-T") + t.AddArgument(tmp_dir) + t.AddArgument("-V") + t.AddArgument(baseline_img) + + # Perform the image comparison test and print out the result. + return t.RegressionTest(test_img, 0.05) + + +# ============================================================================= +# Provide a wait function +# ============================================================================= +def wait_with_timeout(delay=None, limit=0, criterion=None): + """ + This function provides the ability to wait for a certain number of seconds, + or else to wait for a specific criterion to be met. + """ + for i in itertools.count(): + if criterion is not None and criterion(): + return True + elif delay * i > limit: + return False + else: + time.sleep(delay) + + +# ============================================================================= +# Define a WebTest class with five stages of testing: initialization, setup, +# capture, postprocess, and cleanup. +# ============================================================================= +class WebTest(object): + """ + This is the base class for all automated web-based tests. It defines five + stages that any test must run through, and allows any or all of these + stages to be overridden by subclasses. This class defines the run_test + method to invoke the five stages overridden by subclasses, one at a time: + 1) initialize, 2) setup, 3) capture, 4) postprocess, and 5) cleanup. + """ + + class Abort: + pass + + def __init__(self, url=None, testname=None, **kwargs): + self.url = url + self.testname = testname + + def run_test(self): + try: + self.checkdependencies() + self.initialize() + self.setup() + self.capture() + self.postprocess() + except WebTest.Abort: + # Placeholder for future option to return failure result + pass + except: + self.cleanup() + raise + + self.cleanup() + + def checkdependencies(self): + pass + + def initialize(self): + pass + + def setup(self): + pass + + def capture(self): + pass + + def postprocess(self): + pass + + def cleanup(self): + pass + + +# ============================================================================= +# Define a WebTest subclass designed specifically for browser-based tests. +# ============================================================================= +class BrowserBasedWebTest(WebTest): + """ + This class can be used as a base for any browser-based web tests. It + introduces the notion of a selenium browser and overrides phases (1) and + (3), initialization and cleanup, of the test phases introduced in the base + class. Initialization involves selecting the browser type, setting the + browser window size, and asking the browser to load the url. Cleanup + involves closing the browser window. + """ + + def __init__(self, size=None, browser=None, **kwargs): + self.size = size + self.browser = browser + self.window = None + + WebTest.__init__(self, **kwargs) + + def initialize(self): + try: + if self.browser is None or self.browser == TestModuleBrowsers.chrome: + self.window = webdriver.Chrome() + elif self.browser == TestModuleBrowsers.firefox: + self.window = webdriver.Firefox() + elif self.browser == TestModuleBrowsers.internet_explorer: + self.window = webdriver.Ie() + else: + raise DependencyError( + "self.browser argument has illegal value %r" % (self.browser) + ) + except DependencyError as dErr: + raise + except Exception as inst: + raise DependencyError(inst) + + if self.size is not None: + self.window.set_window_size(self.size[0], self.size[1]) + + if self.url is not None: + self.window.get(self.url) + + def cleanup(self): + try: + self.window.quit() + except: + print( + "Unable to call window.quit, perhaps this is expected because of unmet browser dependency." + ) + + +# ============================================================================= +# Extend BrowserBasedWebTest to handle vtk-style image comparison +# ============================================================================= +class ImageComparatorWebTest(BrowserBasedWebTest): + """ + This class extends browser based web tests to include image comparison. It + overrides the capture phase of testing with some functionality to simply + grab a screenshot of the entire browser window. It overrides the + postprocess phase with a call to vtk image comparison functionality. + Derived classes can then simply override the setup function with a series + of selenium-based browser interactions to create a complete test. Derived + classes may also prefer to override the capture phase to capture only + certain portions of the browser window for image comparison. + """ + + def __init__(self, filename=None, baseline=None, temporaryDir=None, **kwargs): + if filename is None: + raise TypeError("missing argument 'filename'") + if baseline is None: + raise TypeError("missing argument 'baseline'") + + BrowserBasedWebTest.__init__(self, **kwargs) + self.filename = filename + self.baseline = baseline + self.tmpDir = temporaryDir + + def capture(self): + self.window.save_screenshot(self.filename) + + def postprocess(self): + result = compare_images(self.filename, self.baseline, self.tmpDir) + + if result == 1: + test_pass(self.testname) + else: + test_fail(self.testname) + + +# ============================================================================= +# Given a css selector to use in finding the image element, get the element, +# then base64 decode the "src" attribute and return it. +# ============================================================================= +def get_image_data(browser, cssSelector): + """ + This function takes a selenium browser and a css selector string and uses + them to find the target HTML image element. The desired image element + should contain it's image data as a Base64 encoded JPEG image string. + The 'src' attribute of the image is read, Base64-decoded, and then + returned. + + browser: A selenium browser instance, as created by webdriver.Chrome(), + for example. + + cssSelector: A string containing a CSS selector which will be used to + find the HTML image element of interest. + """ + + # Here's maybe a better way to get at that image element + imageElt = browser.find_element_by_css_selector(cssSelector) + + # Now get the Base64 image string and decode it into image data + base64String = imageElt.get_attribute("src") + b64RegEx = re.compile(r"data:image/jpeg;base64,(.+)") + b64Matcher = b64RegEx.match(base64String) + imgdata = base64.b64decode(b64Matcher.group(1)) + + return imgdata + + +# ============================================================================= +# Combines a variation on above function with the write_image_to_disk function. +# converting jpg to png in the process, if necessary. +# ============================================================================= +def save_image_data_as_png(browser, cssSelector, imgfilename): + """ + This function takes a selenium browser instance, a css selector string, + and a file name. It uses the css selector string to finds the target HTML + Image element, which should contain a Base64 encoded JPEG image string, + it decodes the string to image data, and then saves the data to the file. + The image type of the written file is determined from the extension of the + provided filename. + + browser: A selenium browser instance as created by webdriver.Chrome(), + for example. + + cssSelector: A string containing a CSS selector which will be used to + find the HTML image element of interest. + + imgFilename: The filename to which to save the image. The extension is + used to determine the type of image which should be saved. + """ + imageElt = browser.find_element_by_css_selector(cssSelector) + base64String = imageElt.get_attribute("src") + b64RegEx = re.compile(r"data:image/jpeg;base64,(.+)") + b64Matcher = b64RegEx.match(base64String) + img = Image.open(io.BytesIO(base64.b64decode(b64Matcher.group(1)))) + img.save(imgfilename) + + +# ============================================================================= +# Given a decoded image and the full path to a file, write the image to the +# file. +# ============================================================================= +def write_image_to_disk(imgData, filePath): + """ + This function takes an image data, as returned by this module's + get_image_data() function for example, and writes it out to the file given by + the filePath parameter. + + imgData: An image data object + filePath: The full path, including the file name and extension, where + the image should be written. + """ + + with open(filePath, "wb") as f: + f.write(imgData) + + +# ============================================================================= +# There could be problems if the script file has more than one class defn which +# is a subclass of vtk.web.testing.WebTest, so we should write some +# documentation to help people avoid that. +# ============================================================================= +def instantiate_test_subclass(pathToScript, **kwargs): + """ + This function takes the fully qualified path to a test file, along with + any needed keyword arguments, then dynamically loads the file as a module + and finds the test class defined inside of it via inspection. It then + uses the keyword arguments to instantiate the test class and return the + instance. + + pathToScript: Fully qualified path to python file containing defined + subclass of one of the test base classes. + kwargs: Keyword arguments to be passed to the constructor of the + testing subclass. + """ + + # Load the file as a module + moduleName = imp.load_source("dynamicTestModule", pathToScript) + instance = None + + # Inspect dynamically loaded module members + for name, obj in inspect.getmembers(moduleName): + # Looking for classes only + if inspect.isclass(obj): + instance = obj.__new__(obj) + # And only classes defined in the dynamically loaded module + if instance.__module__ == "dynamicTestModule": + try: + instance.__init__(**kwargs) + break + except Exception as inst: + print("Caught exception: " + str(type(inst))) + print(inst) + raise + + return instance + + +# ============================================================================= +# For testing purposes, define a function which can interact with a running +# paraview or vtk web application service. +# ============================================================================= +def launch_web_test(*args, **kwargs): + """ + This function loads a python file as a module (with no package), and then + instantiates the class it must contain, and finally executes the run_test() + method of the class (which the class may override, but which is defined in + both of the testing base classes, WebTest and ImageComparatorBaseClass). + After the run_test() method finishes, this function will stop the web + server if required. This function expects some keyword arguments will be + present in order for it to complete it's task: + + kwargs['serverOpts']: An object containing all the parameters used + to start the web service. Some of them will be used in the test script + in order perform the test. For example, the port on which the server + was started will be required in order to connect to the server. + + kwargs['testScript']: The full path to the python file containing the + testing subclass. + """ + + serverOpts = None + testScriptFile = None + + # This is really the thing all test scripts will need: access to all + # the options used to start the server process. + if "serverOpts" in kwargs: + serverOpts = kwargs["serverOpts"] + # print 'These are the serverOpts we got: ' + # print serverOpts + + # Get the full path to the test script + if "testScript" in kwargs: + testScriptFile = kwargs["testScript"] + + testName = "unknown" + + # Check for a test file (python file) + if testScriptFile is None: + print("No test script file found, no test script will be run.") + test_fail(testName) + + # The test name will be generated from the python script name, so + # match and capture a bunch of contiguous characters which are + # not '.', '\', or '/', followed immediately by the string '.py'. + fnamePattern = re.compile("([^\.\/\\\]+)\.py") + fmatch = re.search(fnamePattern, testScriptFile) + if fmatch: + testName = fmatch.group(1) + else: + print( + "Unable to parse testScriptFile (" + + str(testScriptfile) + + "), no test will be run" + ) + test_fail(testName) + + # If we successfully got a test name, we are ready to try and run the test + if testName != "unknown": + + # Output file and baseline file names are generated from the test name + imgFileName = testName + ".png" + knownGoodFileName = concat_paths(serverOpts.baselineImgDir, imgFileName) + tempDir = serverOpts.tmpDirectory + testImgFileName = serverOpts.testImgFile + + testBrowser = test_module_browsers.index(serverOpts.useBrowser) + + # Now try to instantiate and run the test + try: + testInstance = instantiate_test_subclass( + testScriptFile, + testname=testName, + host=serverOpts.host, + port=serverOpts.port, + browser=testBrowser, + filename=testImgFileName, + baseline=knownGoodFileName, + temporaryDir=tempDir, + ) + + # If we were able to instantiate the test, run it, otherwise we + # consider it a failure. + if testInstance is not None: + try: + testInstance.run_test() + except DependencyError as derr: + # TODO: trigger return SKIP_RETURN_CODE when CMake 3 is required + print( + "Some dependency of this test was not met, allowing it to pass" + ) + test_pass(testName) + else: + print("Unable to instantiate test instance, failing test") + test_fail(testName) + return + + except Exception as inst: + import sys, traceback + + tb = sys.exc_info()[2] + print("Caught an exception while running test script:") + print(" " + str(type(inst))) + print(" " + str(inst)) + print(" " + "".join(traceback.format_tb(tb))) + test_fail(testName) + + # If we were passed a cleanup method to run after testing, invoke it now + if "cleanupMethod" in serverOpts: + serverOpts["cleanupMethod"]() + + +# ============================================================================= +# To keep the service module clean, we'll process the test results here, given +# the test result object we generated in "launch_web_test". It is +# passed back to this function after the service has completed. Failure of +# of the test is indicated by raising an exception in here. +# ============================================================================= +def finalize(): + """ + This function checks the module's global test_module_comm_queue variable for a + test result. If one is found and the result is 'fail', then this function + raises an exception to communicate the failure to the CTest framework. + + In order for a test result to be found in the test_module_comm_queue variable, + the test script must have called either the testPass or testFail functions + provided by this test module before returning. + """ + + global test_module_comm_queue + + if test_module_comm_queue is not None: + resultObject = test_module_comm_queue.get() + + failedATest = False + + for testName in resultObject: + testResult = resultObject[testName] + if testResult == "fail": + print(" Test -> " + testName + ": " + testResult) + failedATest = True + + if failedATest is True: + raise Exception( + "At least one of the requested tests failed. " + + "See detailed output, above, for more information" + ) diff --git a/Web/Python/vtkmodules/web/utils.py b/Web/Python/vtkmodules/web/utils.py new file mode 100644 index 000000000..1a49112a6 --- /dev/null +++ b/Web/Python/vtkmodules/web/utils.py @@ -0,0 +1,211 @@ +try: + import numpy as np +except ImportError: + raise ImportError( + "This module depends on the numpy module. Please make\ +sure that it is installed properly." + ) + +import base64 + +from vtkmodules.util.numpy_support import vtk_to_numpy +from vtkmodules.vtkFiltersGeometry import vtkDataSetSurfaceFilter + +# Numpy to JS TypedArray +to_js_type = { + "int8": "Int8Array", + "uint8": "Uint8Array", + "int16": "Int16Array", + "uint16": "Uint16Array", + "int32": "Int32Array", + "uint32": "Uint32Array", + "int64": "Int32Array", + "uint64": "Uint32Array", + "float32": "Float32Array", + "float64": "Float64Array", +} + + +def b64_encode_numpy(obj): + # Convert 1D numpy arrays with numeric types to memoryviews with + # datatype and shape metadata. + if len(obj) == 0: + return obj.tolist() + + dtype = obj.dtype + if dtype.kind == "f": + return np_encode(obj) + elif dtype.kind == "b": + return np_encode(obj, np.uint8) + elif dtype.kind in ["u", "i"]: + # Try to see if we can downsize the array + max_value = np.amax(obj) + min_value = np.amin(obj) + signed = min_value < 0 + test_value = max(max_value, -min_value) + if signed: + if test_value < np.iinfo(np.int8): + return np_encode(obj, np.int8) + if test_value < np.iinfo(np.int16).max: + return np_encode(obj, np.int16) + if test_value < np.iinfo(np.int32).max: + return np_encode(obj, np.int32) + else: + if test_value < np.iinfo(np.uint8).max: + return np_encode(obj, np.uint8) + if test_value < np.iinfo(np.uint16).max: + return np_encode(obj, np.uint16) + if test_value < np.iinfo(np.uint32).max: + return np_encode(obj, np.uint32) + + # Convert all other numpy arrays to lists + return obj.tolist() + + +def np_encode(array, np_type=None): + if np_type: + n_array = array.astype(np_type).ravel(order="C") + return { + "bvals": base64.b64encode(memoryview(n_array)).decode("utf-8"), + "dtype": str(n_array.dtype), + "shape": list(array.shape), + } + return { + "bvals": base64.b64encode(memoryview(array.ravel(order="C"))).decode("utf-8"), + "dtype": str(array.dtype), + "shape": list(array.shape), + } + + +def mesh_array(array): + if array: + return b64_encode_numpy(vtk_to_numpy(array.GetData())) + + +def data_array(data_array, location="PointData", name=None): + if data_array: + dataRange = data_array.GetRange(-1) + nb_comp = data_array.GetNumberOfComponents() + values = vtk_to_numpy(data_array) + js_types = to_js_type[str(values.dtype)] + return { + "name": name if name else data_array.GetName(), + "values": b64_encode_numpy(values), + "numberOfComponents": nb_comp, + "type": js_types, + "location": location, + "dataRange": dataRange, + } + + +def field_data(field_data, names, location="PointData"): + fields = [] + for name in names: + array = field_data.GetArray(name) + js_array = data_array(array, location, name) + if js_array: + fields.append(js_array) + + return fields + + +def mesh(dataset, field_to_keep=None, point_arrays=None, cell_arrays=None): + """Expect any dataset and extract its surface into a dash_vtk.Mesh state property""" + if dataset is None: + return None + + # Make sure we have a polydata to export + polydata = None + if dataset.IsA("vtkPolyData"): + polydata = dataset + else: + extractSkinFilter = vtkDataSetSurfaceFilter() + extractSkinFilter.SetInputData(dataset) + extractSkinFilter.Update() + polydata = extractSkinFilter.GetOutput() + + if polydata.GetPoints() is None: + return None + + # Extract mesh + state = {"mesh": {}} + + points = mesh_array(polydata.GetPoints()) + if points: + state["mesh"]["points"] = points + + verts = mesh_array(polydata.GetVerts()) + if verts: + state["mesh"]["verts"] = verts + + lines = mesh_array(polydata.GetLines()) + if lines: + state["mesh"]["lines"] = lines + + polys = mesh_array(polydata.GetPolys()) + if polys: + state["mesh"]["polys"] = polys + + strips = mesh_array(polydata.GetStrips()) + if strips: + state["mesh"]["strips"] = strips + + # Scalars + if field_to_keep is not None: + field = None + p_array = polydata.GetPointData().GetArray(field_to_keep) + c_array = polydata.GetCellData().GetArray(field_to_keep) + + if c_array: + field = data_array(c_array, location="CellData", name=field_to_keep) + + if p_array: + field = data_array(p_array, location="PointData", name=field_to_keep) + + if field: + state.update({"field": field}) + + # PointData Fields + if point_arrays: + point_data = field_data(polydata.GetPointData(), point_arrays, "PointData") + if len(point_data): + state.update({"pointArrays": point_data}) + + # CellData Fields + if cell_arrays: + cell_data = field_data(polydata.GetCellData(), cell_arrays, "CellData") + if len(cell_data): + state.update({"cellArrays": cell_data}) + + return state + + +def volume(dataset): + """Expect a vtkImageData and extract its setting for the dash_vtk.Volume state""" + if dataset is None or not dataset.IsA("vtkImageData"): + return None + + state = { + "image": { + "dimensions": dataset.GetDimensions(), + "spacing": dataset.GetSpacing(), + "origin": dataset.GetOrigin(), + }, + } + + # Capture image orientation if any + if hasattr(dataset, "GetDirectionMatrix"): + matrix = dataset.GetDirectionMatrix() + js_mat = [] + for j in range(3): + for i in range(3): + js_mat.append(matrix.GetElement(i, j)) + + state["image"]["direction"] = js_mat + + scalars = dataset.GetPointData().GetScalars() + field = data_array(scalars, location="PointData") + if field: + state["field"] = field + + return state diff --git a/Web/Python/vtkmodules/web/venv.py b/Web/Python/vtkmodules/web/venv.py new file mode 100644 index 000000000..3bef60844 --- /dev/null +++ b/Web/Python/vtkmodules/web/venv.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +"""Activate venv for current interpreter: + +Use `from vtk.web import venv` along one of the following + - `--venv /path/to/venv/base` argument + - environment variable `VTK_VENV=/path/to/venv/base` + +This can be used when you must use an existing Python interpreter, not the venv bin/python. +""" +import os +import site +import sys + +VENV_BASE = None +VENV_LOADED = False + +if "--venv" in sys.argv: + VENV_BASE = os.path.abspath(sys.argv[sys.argv.index("--venv") + 1]) + +if os.environ.get("VTK_VENV"): + VENV_BASE = os.path.abspath(os.environ.get("VTK_VENV")) + +if not VENV_LOADED and VENV_BASE and os.path.exists(VENV_BASE): + VENV_LOADED = True + # Code inspired by virutal-env::bin/active_this.py + bin_dir = os.path.join(VENV_BASE, "bin") + os.environ["PATH"] = os.pathsep.join([bin_dir] + os.environ.get("PATH", "").split(os.pathsep)) + os.environ["VIRTUAL_ENV"] = VENV_BASE + prev_length = len(sys.path) + python_libs = os.path.join(VENV_BASE, f"lib/python{sys.version_info.major}.{sys.version_info.minor}/site-packages") + site.addsitedir(python_libs) + sys.path[:] = sys.path[prev_length:] + sys.path[0:prev_length] + sys.real_prefix = sys.prefix + sys.prefix = VENV_BASE + # + print(f"VTK is using venv: {VENV_BASE}") diff --git a/Web/Python/vtkmodules/web/vtkjs_helper.py b/Web/Python/vtkmodules/web/vtkjs_helper.py new file mode 100644 index 000000000..4a2f6ae36 --- /dev/null +++ b/Web/Python/vtkmodules/web/vtkjs_helper.py @@ -0,0 +1,283 @@ +import base64 +import json +import re +import os +import shutil +import sys +import zipfile + +try: + import zlib + + compression = zipfile.ZIP_DEFLATED +except: + compression = zipfile.ZIP_STORED + +# ----------------------------------------------------------------------------- + + +def convertDirectoryToZipFile(directoryPath): + if os.path.isfile(directoryPath): + return + + zipFilePath = "%s.zip" % directoryPath + zf = zipfile.ZipFile(zipFilePath, mode="w") + + try: + for dirName, subdirList, fileList in os.walk(directoryPath): + for fname in fileList: + fullPath = os.path.join(dirName, fname) + relPath = "%s" % (os.path.relpath(fullPath, directoryPath)) + zf.write(fullPath, arcname=relPath, compress_type=compression) + finally: + zf.close() + + shutil.rmtree(directoryPath) + shutil.move(zipFilePath, directoryPath) + + +# ----------------------------------------------------------------------------- + + +def addDataToViewer(dataPath, srcHtmlPath, disableGirder=False): + if os.path.isfile(dataPath) and os.path.exists(srcHtmlPath): + dstDir = os.path.dirname(dataPath) + dstHtmlPath = os.path.join(dstDir, "%s.html" % os.path.basename(dataPath)[:-6]) + + # Extract data as base64 + with open(dataPath, "rb") as data: + dataContent = data.read() + base64Content = base64.b64encode(dataContent) + base64Content = base64Content.decode().replace("\n", "") + + # Create new output file + with open(srcHtmlPath, mode="r", encoding="utf-8") as srcHtml: + with open(dstHtmlPath, mode="w", encoding="utf-8") as dstHtml: + for line in srcHtml: + if disableGirder and "" in line: + dstHtml.write( + """ + + """ + ) + if "" in line: + dstHtml.write("\n") + + dstHtml.write(line) + + +# ----------------------------------------------------------------------------- + + +def numericSorted(l): + """Numerically sort a list of strings.""" + + # pattern to split name into numeric and non-numeric parts + splitter_pattern = re.compile('([0-9]+|[^0-9]+)') + + def keyfunc(name): + """Sorting key for numeric sorting.""" + split_name = re.findall(splitter_pattern, name) + # one-liner to convert numeric parts into integers + split_name = list(map(lambda x: int(x) if x.isdigit() else x, split_name)) + # ensure that list begins with a string to avoid string<->int compare + if split_name and isinstance(split_name[0], int): + split_name.insert(0, '') + return split_name + + # return the numerically sorted list + return sorted(l, key=keyfunc) + + +# ----------------------------------------------------------------------------- + + +def zipAllTimeSteps(directoryPath): + if os.path.isfile(directoryPath): + return + + class UrlCounterDict(dict): + Counter = 0 + + def GetUrlName(self, name): + if name not in self.keys(): + self[name] = str(objNameToUrls.Counter) + self.Counter = self.Counter + 1 + return self[name] + + def InitIndex(sourcePath, destObj): + with open(sourcePath, "r") as sourceFile: + sourceData = sourceFile.read() + sourceObj = json.loads(sourceData) + for key in sourceObj: + destObj[key] = sourceObj[key] + # remove vtkHttpDataSetReader information + for obj in destObj["scene"]: + obj.pop(obj["type"]) + obj.pop("type") + + def getUrlToNameDictionary(indexObj): + urls = {} + for obj in indexObj["scene"]: + urls[obj[obj["type"]]["url"]] = obj["name"] + return urls + + def addDirectoryToZip( + dirname, zipobj, storedData, rootIdx, timeStep, objNameToUrls + ): + # Update root index.json file from index.json of this timestep + with open(os.path.join(dirname, "index.json"), "r") as currentIdxFile: + currentIdx = json.loads(currentIdxFile.read()) + urlToName = getUrlToNameDictionary(currentIdx) + rootTimeStepSection = rootIdx["animation"]["timeSteps"][timeStep] + for key in currentIdx: + if key == "scene" or key == "version": + continue + rootTimeStepSection[key] = currentIdx[key] + for obj in currentIdx["scene"]: + objName = obj["name"] + rootTimeStepSection[objName] = {} + rootTimeStepSection[objName]["actor"] = obj["actor"] + rootTimeStepSection[objName]["actorRotation"] = obj["actorRotation"] + rootTimeStepSection[objName]["mapper"] = obj["mapper"] + rootTimeStepSection[objName]["property"] = obj["property"] + + # For every object in the current timestep + for folder in sorted(os.listdir(dirname)): + currentItem = os.path.join(dirname, folder) + if os.path.isdir(currentItem) is False: + continue + # Write all data array of the current timestep in the archive + for filename in os.listdir(os.path.join(currentItem, "data")): + fullpath = os.path.join(currentItem, "data", filename) + if os.path.isfile(fullpath) and filename not in storedData: + storedData.add(filename) + relPath = os.path.join("data", filename) + zipobj.write(fullpath, arcname=relPath, compress_type=compression) + # Write the index.json containing pointers to these data arrays + # while replacing every basepath as '../../data' + objIndexFilePath = os.path.join(dirname, folder, "index.json") + with open(objIndexFilePath, "r") as objIndexFile: + objIndexObjData = json.loads(objIndexFile.read()) + for elm in objIndexObjData.keys(): + try: + if "ref" in objIndexObjData[elm].keys(): + objIndexObjData[elm]["ref"]["basepath"] = "../../data" + if "arrays" in objIndexObjData[elm].keys(): + for array in objIndexObjData[elm]["arrays"]: + array["data"]["ref"]["basepath"] = "../../data" + except AttributeError: + continue + currentObjName = urlToName[folder] + objIndexRelPath = os.path.join( + objNameToUrls.GetUrlName(currentObjName), str(timeStep), "index.json" + ) + zipobj.writestr( + objIndexRelPath, + json.dumps(objIndexObjData, indent=2), + compress_type=compression, + ) + + # --- + + zipFilePath = "%s.zip" % directoryPath + currentDirectory = os.path.abspath(os.path.join(directoryPath, os.pardir)) + rootIndexPath = os.path.join(currentDirectory, "index.json") + rootIndexFile = open(rootIndexPath, "r") + rootIndexObj = json.loads(rootIndexFile.read()) + + zf = zipfile.ZipFile(zipFilePath, mode="w") + try: + # We copy the scene from an index of a specific timestep to the root index + # Scenes should all have the same objects so only do it for the first one + isSceneInitialized = False + # currentlyAddedData set stores hashes of every data we already added to the + # vtkjs archive to prevent data duplication + currentlyAddedData = set() + # Regex that folders storing timestep data from paraview should follow + reg = re.compile(r"^" + os.path.basename(directoryPath) + r"\.[0-9]+$") + # We assume an object will not be deleted from a timestep to another so we create a generic index.json for each object + genericIndexObj = {} + genericIndexObj["series"] = [] + timeStep = 0 + for item in rootIndexObj["animation"]["timeSteps"]: + genericIndexObj["series"].append({}) + genericIndexObj["series"][timeStep]["url"] = str(timeStep) + genericIndexObj["series"][timeStep]["timeStep"] = float(item["time"]) + timeStep = timeStep + 1 + # Keep track of the url for every object + objNameToUrls = UrlCounterDict() + + timeStep = 0 + # zip all timestep directories + for folder in numericSorted(os.listdir(currentDirectory)): + fullPath = os.path.join(currentDirectory, folder) + if os.path.isdir(fullPath) and reg.match(folder): + if not isSceneInitialized: + InitIndex(os.path.join(fullPath, "index.json"), rootIndexObj) + isSceneInitialized = True + addDirectoryToZip( + fullPath, + zf, + currentlyAddedData, + rootIndexObj, + timeStep, + objNameToUrls, + ) + shutil.rmtree(fullPath) + timeStep = timeStep + 1 + + # Write every index.json holding time information for each object + for name in objNameToUrls: + zf.writestr( + os.path.join(objNameToUrls[name], "index.json"), + json.dumps(genericIndexObj, indent=2), + compress_type=compression, + ) + + # Update root index.json urls and write it in the archive + for obj in rootIndexObj["scene"]: + obj["id"] = obj["name"] + obj["type"] = "vtkHttpDataSetSeriesReader" + obj["vtkHttpDataSetSeriesReader"] = {} + obj["vtkHttpDataSetSeriesReader"]["url"] = objNameToUrls[obj["name"]] + zf.writestr( + "index.json", json.dumps(rootIndexObj, indent=2), compress_type=compression + ) + os.remove(rootIndexPath) + + finally: + zf.close() + + shutil.move(zipFilePath, directoryPath) + + +# ----------------------------------------------------------------------------- +# Main +# ----------------------------------------------------------------------------- + +if __name__ == "__main__": + if len(sys.argv) < 2: + print( + "Usage: directoryToFile /path/to/directory.vtkjs [/path/to/ParaViewGlance.html]" + ) + else: + fileName = sys.argv[1] + convertDirectoryToZipFile(fileName) + + if len(sys.argv) == 3: + addDataToViewer(fileName, sys.argv[2]) diff --git a/Web/Python/vtkmodules/web/wslink.py b/Web/Python/vtkmodules/web/wslink.py new file mode 100644 index 000000000..20cf68e5b --- /dev/null +++ b/Web/Python/vtkmodules/web/wslink.py @@ -0,0 +1,67 @@ +r"""wslink is a module that extends any +wslink related classes for the purposes of vtkWeb. + +""" + +from __future__ import absolute_import, division, print_function + +# import inspect, types, string, random, logging, six, json, re, base64 +import json, base64, logging, time + +from vtkmodules.web.errors import WebDependencyMissingError + +try: + from wslink import websocket + from wslink import register as exportRpc +except ImportError: + raise WebDependencyMissingError() + +from vtkmodules.web import protocols +from vtkmodules.vtkWebCore import vtkWebApplication + +# ============================================================================= +application = None + +# ============================================================================= +# +# Base class for vtkWeb ServerProtocol +# +# ============================================================================= + + +class ServerProtocol(websocket.ServerProtocol): + """ + Defines the core server protocol for vtkWeb. Adds support to + marshall/unmarshall RPC callbacks that involve ServerManager proxies as + arguments or return values. + + Applications typically don't use this class directly, but instead + sub-class it and call self.registerVtkWebProtocol() with useful vtkWebProtocols. + """ + + def __init__(self): + logging.info("Creating SP") + self.setSharedObject("app", self.initApplication()) + websocket.ServerProtocol.__init__(self) + + def initApplication(self): + """ + Let subclass optionally initialize a custom application in lieu + of the default vtkWebApplication. + """ + global application + if not application: + application = vtkWebApplication() + return application + + def setApplication(self, application): + self.setSharedObject("app", application) + + def getApplication(self): + return self.getSharedObject("app") + + def registerVtkWebProtocol(self, protocol): + self.registerLinkProtocol(protocol) + + def getVtkWebProtocols(self): + return self.getLinkProtocols() diff --git a/Web/WebAssembly/CMakeLists.txt b/Web/WebAssembly/CMakeLists.txt new file mode 100644 index 000000000..9df85717c --- /dev/null +++ b/Web/WebAssembly/CMakeLists.txt @@ -0,0 +1,128 @@ +if (NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten") + message(FATAL_ERROR + "The VTK::WebAssembly module requires Emscripten compiler.") +endif () + +set(classes + vtkWasmSceneManager) + +vtk_module_add_module(VTK::WebAssembly + CLASSES ${classes}) + +vtk_add_test_mangling(VTK::WebAssembly) + +set(_vtk_wasm_scene_manager_autoinit_mods) +get_property(_vtk_wasm_scene_manager_optional_deps GLOBAL + PROPERTY "_vtk_module_VTK::WebAssembly_optional_depends") +foreach(_module IN LISTS _vtk_wasm_scene_manager_private_deps _vtk_wasm_scene_manager_optional_deps) + if (NOT TARGET "${_module}") + continue () + endif () + list(APPEND _vtk_wasm_scene_manager_autoinit_mods "${_module}") +endforeach() +vtk_module_autoinit( + TARGETS WebAssembly + MODULES ${_vtk_wasm_scene_manager_autoinit_mods}) +# ----------------------------------------------------------------------------- +# Emscripten compile+link options +# ----------------------------------------------------------------------------- +set(emscripten_link_options) +list(APPEND emscripten_link_options + "-lembind" + "--extern-post-js=${CMAKE_CURRENT_SOURCE_DIR}/post.js" + # "--embind-emit-tsd=vtkWasmSceneManager.ts" + #"--memoryprofiler" + #"--cpuprofiler" + "-sALLOW_MEMORY_GROWTH=1" + "-sALLOW_TABLE_GROWTH=1" + "-sEXPORT_NAME=vtkWasmSceneManager" + "-sENVIRONMENT=node,web" + "-sEXPORTED_RUNTIME_METHODS=['addFunction','UTF8ToString','FS']" + # "-sEXCEPTION_DEBUG=1" # prints stack trace for uncaught C++ exceptions from VTK (very rare, but PITA to figure out) + # "-sGL_DEBUG=1" + # "-sGL_ASSERTIONS=1" + # "-sTRACE_WEBGL_CALLS=1" + ) +if (CMAKE_SIZEOF_VOID_P EQUAL "8") + list(APPEND emscripten_link_options + "-sMAXIMUM_MEMORY=16GB") +else () + list(APPEND emscripten_link_options + "-sMAXIMUM_MEMORY=4GB") +endif () +# ----------------------------------------------------------------------------- +# Optimizations +# ----------------------------------------------------------------------------- +set(emscripten_optimizations) +set(emscripten_debug_options) +if (CMAKE_BUILD_TYPE STREQUAL "Release") + set(vtk_scene_manager_wasm_optimize "BEST") + set(vtk_scene_manager_wasm_debuginfo "NONE") +elseif (CMAKE_BUILD_TYPE STREQUAL "MinSizeRel") + set(vtk_scene_manager_wasm_optimize "SMALLEST_WITH_CLOSURE") + set(vtk_scene_manager_wasm_debuginfo "NONE") +elseif (CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo") + set(vtk_scene_manager_wasm_optimize "MORE") + set(vtk_scene_manager_wasm_debuginfo "PROFILE") +elseif (CMAKE_BUILD_TYPE STREQUAL "Debug") + set(vtk_scene_manager_wasm_optimize "NO_OPTIMIZATION") + set(vtk_scene_manager_wasm_debuginfo "DEBUG_NATIVE") +endif () +set(vtk_scene_manager_wasm_optimize_NO_OPTIMIZATION "-O0") +set(vtk_scene_manager_wasm_optimize_LITTLE "-O1") +set(vtk_scene_manager_wasm_optimize_MORE "-O2") +set(vtk_scene_manager_wasm_optimize_BEST "-O3") +set(vtk_scene_manager_wasm_optimize_SMALLEST "-Os") +set(vtk_scene_manager_wasm_optimize_SMALLEST_WITH_CLOSURE "-Oz") +set(vtk_scene_manager_wasm_optimize_SMALLEST_WITH_CLOSURE_link "--closure=1") + +if (DEFINED "vtk_scene_manager_wasm_optimize_${vtk_scene_manager_wasm_optimize}") + list(APPEND emscripten_optimizations + ${vtk_scene_manager_wasm_optimize_${vtk_scene_manager_wasm_optimize}}) + list(APPEND emscripten_link_options + ${vtk_scene_manager_wasm_optimize_${vtk_scene_manager_wasm_optimize}_link}) +else () + message (FATAL_ERROR "Unrecognized value for vtk_scene_manager_wasm_optimize=${vtk_scene_manager_wasm_optimize}") +endif () + +set(vtk_scene_manager_wasm_debuginfo_NONE "-g0") +set(vtk_scene_manager_wasm_debuginfo_READABLE_JS "-g1") +set(vtk_scene_manager_wasm_debuginfo_PROFILE "-g2") +set(vtk_scene_manager_wasm_debuginfo_DEBUG_NATIVE "-g3") +set(vtk_scene_manager_wasm_debuginfo_DEBUG_NATIVE_link "-sASSERTIONS=1") +if (DEFINED "vtk_scene_manager_wasm_debuginfo_${vtk_scene_manager_wasm_debuginfo}") + list(APPEND emscripten_debug_options + ${vtk_scene_manager_wasm_debuginfo_${vtk_scene_manager_wasm_debuginfo}}) + list(APPEND emscripten_link_options + ${vtk_scene_manager_wasm_debuginfo_${vtk_scene_manager_wasm_debuginfo}_link}) +else () + message (FATAL_ERROR "Unrecognized value for vtk_scene_manager_wasm_debuginfo=${vtk_scene_manager_wasm_debuginfo}") +endif () + +vtk_module_add_executable(WasmSceneManager + BASENAME vtkWasmSceneManager + vtkWasmSceneManagerEmBinding.cxx) +add_executable("VTK::WasmSceneManager" ALIAS + WasmSceneManager) +target_link_libraries(WasmSceneManager + PRIVATE + VTK::WebAssembly) +target_compile_options(WasmSceneManager + PRIVATE + ${emscripten_compile_options} + ${emscripten_optimizations} + ${emscripten_debug_options}) +target_link_options(WasmSceneManager + PRIVATE + ${emscripten_link_options} + ${emscripten_optimizations} + ${emscripten_debug_options}) +set_target_properties(WasmSceneManager + PROPERTIES + OUTPUT_NAME "vtkWasmSceneManager" + SUFFIX ".mjs") +# [cmake/cmake#20745](https://gitlab.kitware.com/cmake/cmake/-/issues/20745) +# CMake doesn't install multiple files associated with an executable target. +install(FILES + "$/vtkWasmSceneManager.wasm" + DESTINATION ${CMAKE_INSTALL_BINDIR}) diff --git a/Web/WebAssembly/Testing/CMakeLists.txt b/Web/WebAssembly/Testing/CMakeLists.txt new file mode 100644 index 000000000..a82329dba --- /dev/null +++ b/Web/WebAssembly/Testing/CMakeLists.txt @@ -0,0 +1,8 @@ +vtk_module_test_data( + Data/WasmSceneManager/scalar-bar-widget.blobs.json + Data/WasmSceneManager/scalar-bar-widget.states.json + Data/WasmSceneManager/simple.blobs.json) + +if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten") + add_subdirectory(JavaScript) +endif () diff --git a/Web/WebAssembly/Testing/JavaScript/CMakeLists.txt b/Web/WebAssembly/Testing/JavaScript/CMakeLists.txt new file mode 100644 index 000000000..1a13dd766 --- /dev/null +++ b/Web/WebAssembly/Testing/JavaScript/CMakeLists.txt @@ -0,0 +1,22 @@ +set(vtk_nodejs_min_version "23.8.0") +find_package(NodeJS "${vtk_nodejs_min_version}" REQUIRED) +if (VTK_WEBASSEMBLY_64_BIT) + set(_vtk_node_args "--experimental-wasm-memory64") +endif () +set(_vtk_testing_nodejs_exe "${NodeJS_INTERPRETER}") + +if (CMAKE_HOST_WIN32) + list(APPEND _vtk_node_args + --import "file://$") +else () + list(APPEND _vtk_node_args + --import "$") +endif () +vtk_add_test_module_javascript_node( + testBindRenderWindow.mjs + testBlobs.mjs + testInitialize.mjs,NO_DATA + testInvoke.mjs + testOSMesaRenderWindowPatch.mjs + testSkipProperty.mjs + testStates.mjs) diff --git a/Web/WebAssembly/Testing/JavaScript/testBindRenderWindow.mjs b/Web/WebAssembly/Testing/JavaScript/testBindRenderWindow.mjs new file mode 100644 index 000000000..3c2d57fb8 --- /dev/null +++ b/Web/WebAssembly/Testing/JavaScript/testBindRenderWindow.mjs @@ -0,0 +1,53 @@ +async function testBindRenderWindow() { + const manager = await globalThis.createVTKWasmSceneManager({}); + manager.initialize(); + manager.registerStateJSON( + { + Id: 1, + ClassName: "vtkCocoaRenderWindow", + SuperClassNames: ["vtkRenderWindow"], + Interactor: { Id: 2 }, + "vtk-object-manager-kept-alive": true, + }); + manager.registerStateJSON( + { + Id: 2, + ClassName: "vtkCocoaRenderWindowInteractor", + SuperClassNames: ["vtkRenderWindowInteractor"], + RenderWindow: { Id: 1 }, + }); + manager.updateObjectsFromStates(); + + manager.bindRenderWindow(1, "#my-canvas-id"); + + manager.updateStateFromObject(1); + if (manager.getState(1).CanvasSelector !== "#my-canvas-id") { + throw new Error("CanvasSelector was not set correctly on RenderWindow."); + } + + manager.updateStateFromObject(2); + if (manager.getState(2).CanvasSelector !== "#my-canvas-id") { + throw new Error("CanvasSelector was not set correctly on RenderWindowInteractor."); + } +} +const tests = [ + { + description: "Bind RenderWindow to Canvas", + test: testBindRenderWindow, + }, +]; + +let exitCode = 0; +for (let test of tests) { + try { + await test.test(); + console.log("✓", test.description); + exitCode |= 0; + } + catch (error) { + console.log("x", test.description); + console.log(error); + exitCode |= 1; + } +} +process.exit(exitCode); diff --git a/Web/WebAssembly/Testing/JavaScript/testBlobs.mjs b/Web/WebAssembly/Testing/JavaScript/testBlobs.mjs new file mode 100644 index 000000000..a1ddaf428 --- /dev/null +++ b/Web/WebAssembly/Testing/JavaScript/testBlobs.mjs @@ -0,0 +1,52 @@ +import { readFile } from "fs/promises"; +import path from "path"; + +async function testBlobs() { + const dataDirectoryIndex = process.argv.indexOf("-D") + 1; + if (dataDirectoryIndex <= 0) { + throw new Error("Please provide path to a blobs file using -D"); + } + const dataDirectory = process.argv[dataDirectoryIndex]; + const blobs = JSON.parse(await readFile(path.join(dataDirectory, "Data", "WasmSceneManager", "simple.blobs.json"))); + const manager = await globalThis.createVTKWasmSceneManager({}) + if (!manager.initialize()) { + throw new Error("Failed to initialize scene manager"); + } + + for (let hash in blobs) { + if (!manager.registerBlob(hash, new Uint8Array(blobs[hash].bytes))) { + throw new Error(`Failed to register blob with hash=${hash}`); + } + } + for (let hash in blobs) { + const blob = manager.getBlob(hash); + if (!(blob instanceof Uint8Array)) { + throw new Error(`getBlob did not return a Uint8Array for hash=${hash}`); + } + if (blob.toString() !== blobs[hash].bytes.toString()) { + throw new Error(`blob for hash=${hash} does not match registered blob.`); + } + } +} + +const tests = [ + { + description: "Register blobs with hashes", + test: testBlobs, + }, +]; + +let exitCode = 0; +for (let test of tests) { + try { + await test.test(); + console.log("✓", test.description); + exitCode |= 0; + } + catch (error) { + console.log("x", test.description); + console.log(error); + exitCode |= 1; + } +} +process.exit(exitCode); diff --git a/Web/WebAssembly/Testing/JavaScript/testInitialize.mjs b/Web/WebAssembly/Testing/JavaScript/testInitialize.mjs new file mode 100644 index 000000000..acdae0280 --- /dev/null +++ b/Web/WebAssembly/Testing/JavaScript/testInitialize.mjs @@ -0,0 +1,27 @@ +async function testInitialize() { + const manager = await globalThis.createVTKWasmSceneManager({}); + if (!manager.initialize()) { + throw new Error(); + } +} +const tests = [ + { + description: "Initialize VTK scene manager", + test: testInitialize, + }, +]; + +let exitCode = 0; +for (let test of tests) { + try { + await test.test(); + console.log("✓", test.description); + exitCode |= 0; + } + catch (error) { + console.log("x", test.description); + console.log(error); + exitCode |= 1; + } +} +process.exit(exitCode); diff --git a/Web/WebAssembly/Testing/JavaScript/testInvoke.mjs b/Web/WebAssembly/Testing/JavaScript/testInvoke.mjs new file mode 100644 index 000000000..afdf32183 --- /dev/null +++ b/Web/WebAssembly/Testing/JavaScript/testInvoke.mjs @@ -0,0 +1,49 @@ +async function testInvoke() { + const manager = await globalThis.createVTKWasmSceneManager({}); + manager.initialize(); + // manager.setDeserializerLogVerbosity("INFO"); + // manager.setObjectManagerLogVerbosity("INFO"); + // manager.setInvokerLogVerbosity("INFO"); + manager.registerStateJSON({ + "ClassName": "vtkCamera", "SuperClassNames": ["vtkObject"], "vtk-object-manager-kept-alive": true, "Id": 1 + }); + + manager.updateObjectsFromStates(); + + manager.updateStateFromObject(1); + let state = manager.getState(1); + if (JSON.stringify(state.Position) != JSON.stringify([0, 0, 1])) { + throw new Error("Failed to initialize camera state"); + } + + // Invoke a method named "Elevation" on the camera with argument 10.0 + manager.invoke(1, "Elevation", [10.0]); + + manager.updateStateFromObject(1); + state = manager.getState(1); + if (JSON.stringify(state.Position) != JSON.stringify([0, 0.17364817766693033, 0.9848077530122081])) { + throw new Error("vtkCamera::Elevation(10) did not work!"); + } +} + +const tests = [ + { + description: "Invoke methods", + test: testInvoke, + }, +]; + +let exitCode = 0; +for (let test of tests) { + try { + await test.test(); + console.log("✓", test.description); + exitCode |= 0; + } + catch (error) { + console.log("x", test.description); + console.log(error); + exitCode |= 1; + } +} +process.exit(exitCode); diff --git a/Web/WebAssembly/Testing/JavaScript/testOSMesaRenderWindowPatch.mjs b/Web/WebAssembly/Testing/JavaScript/testOSMesaRenderWindowPatch.mjs new file mode 100644 index 000000000..4a1597a4a --- /dev/null +++ b/Web/WebAssembly/Testing/JavaScript/testOSMesaRenderWindowPatch.mjs @@ -0,0 +1,45 @@ +async function testOSMesaRenderWindowPatch() { + const manager = await globalThis.createVTKWasmSceneManager({}); + manager.initialize(); + manager.registerStateJSON({ + Id: 1, + ClassName: "vtkOSOpenGLRenderWindow", + SuperClassNames: ["vtkWindow", "vtkRenderWindow"], + "vtk-object-manager-kept-alive": true, + }); + if (manager.getState(1).ClassName !== "vtkWebAssemblyOpenGLRenderWindow") { + throw new Error("RenderWindow state was not created as vtkWebAssemblyOpenGLRenderWindow."); + } + manager.updateObjectsFromStates(); + + manager.updateObjectFromStateJSON({ + Id: 1, + ClassName: "vtkOSOpenGLRenderWindow", + SuperClassNames: ["vtkWindow", "vtkRenderWindow"], + "vtk-object-manager-kept-alive": true, + }); + if (manager.getState(1).ClassName !== "vtkWebAssemblyOpenGLRenderWindow") { + throw new Error("RenderWindow state was not updated as vtkWebAssemblyOpenGLRenderWindow."); + } +} +const tests = [ + { + description: "Patch vtkOSOpenGLRenderWindow to vtkWebAssemblyOpenGLRenderWindow", + test: testOSMesaRenderWindowPatch, + }, +]; + +let exitCode = 0; +for (let test of tests) { + try { + await test.test(); + console.log("✓", test.description); + exitCode |= 0; + } + catch (error) { + console.log("x", test.description); + console.log(error); + exitCode |= 1; + } +} +process.exit(exitCode); diff --git a/Web/WebAssembly/Testing/JavaScript/testSkipProperty.mjs b/Web/WebAssembly/Testing/JavaScript/testSkipProperty.mjs new file mode 100644 index 000000000..9b39aa342 --- /dev/null +++ b/Web/WebAssembly/Testing/JavaScript/testSkipProperty.mjs @@ -0,0 +1,59 @@ +async function testSkipProperty() { + const manager = await globalThis.createVTKWasmSceneManager({}); + manager.initialize(); + manager.registerStateJSON({ + "ClassName": "vtkCamera", "SuperClassNames": ["vtkObject"], "vtk-object-manager-kept-alive": true, "Id": 1 + }); + + manager.updateObjectsFromStates(); + + // Skip Position and update object. + manager.skipProperty("vtkOpenGLCamera", "Position"); + manager.updateObjectFromStateJSON({ + "Id": 1, + "Position": [0, 1, 2] + }); + + // Verify property was skipped + manager.updateStateFromObject(1); + let state = manager.getState(1); + if (JSON.stringify(state.Position) == JSON.stringify([0, 1, 2])) { + throw new Error("vtkCamera::Position did not get skipped!"); + } + + // UnSkip Position and update object. + manager.unSkipProperty("vtkOpenGLCamera", "Position"); + manager.updateObjectFromStateJSON({ + "Id": 1, + "Position": [3, 4, 5] + }); + + // Verify property was deserialized + manager.updateStateFromObject(1); + state = manager.getState(1); + if (JSON.stringify(state.Position) != JSON.stringify([3, 4, 5])) { + throw new Error("vtkCamera::Position did not get unskipped!"); + } +} + +const tests = [ + { + description: "Invoke methods", + test: testSkipProperty, + }, +]; + +let exitCode = 0; +for (let test of tests) { + try { + await test.test(); + console.log("✓", test.description); + exitCode |= 0; + } + catch (error) { + console.log("x", test.description); + console.log(error); + exitCode |= 1; + } +} +process.exit(exitCode); diff --git a/Web/WebAssembly/Testing/JavaScript/testStates.mjs b/Web/WebAssembly/Testing/JavaScript/testStates.mjs new file mode 100644 index 000000000..b9219d884 --- /dev/null +++ b/Web/WebAssembly/Testing/JavaScript/testStates.mjs @@ -0,0 +1,60 @@ +import { readFile } from "fs/promises"; +import path from "path"; + +const object_ids = [1, 2, 3, 41, 5, 42, 44, 4, 6, 33, 35, 38, 40, 43, 11, 45, 46, 47, 48, 49, 50, 51, 7, 34, 36, 37, 39, 12, 8, 9, 10, 13, 14, 15, 16, 19, 21, 24, 27, 30, 17, 18, 20, 22, 23, 25, 26, 28, 29, 31, 32] +const exepected_dependencies = [1, 2, 3, 41, 5, 42, 44, 4, 6, 33, 35, 38, 40, 43, 11, 45, 46, 47, 48, 49, 50, 51, 7, 34, 36, 37, 39, 12, 8, 9, 10, 13, 14, 15, 16, 19, 21, 24, 27, 30, 17, 18, 20, 22, 23, 25, 26, 28, 29] + +async function testStates() { + const dataDirectoryIndex = process.argv.indexOf("-D") + 1; + if (dataDirectoryIndex <= 0) { + throw new Error("Please provide path to a blobs file using -D"); + } + const dataDirectory = process.argv[dataDirectoryIndex]; + const blobs = JSON.parse(await readFile(path.join(dataDirectory, "Data", "WasmSceneManager", "scalar-bar-widget.blobs.json"))); + const states = JSON.parse(await readFile(path.join(dataDirectory, "Data", "WasmSceneManager", "scalar-bar-widget.states.json"))); + const manager = await globalThis.createVTKWasmSceneManager({}); + if (!manager.initialize()) { + throw new Error("Failed to initialize scene manager"); + } + for (let i = 0; i < object_ids.length; ++i) { + const object_id = object_ids[i]; + if (!manager.registerState(JSON.stringify(states[object_id]))) { + throw new Error(`Failed to register state at object_id=${object_id}`); + } + } + for (let hash in blobs) { + if (!manager.registerBlob(hash, new Uint8Array(blobs[hash].bytes))) { + throw new Error(`Failed to register blob with hash=${hash}`); + } + } + manager.updateObjectsFromStates(); + const activeIds = manager.getAllDependencies(0); + if (!(activeIds instanceof Uint32Array)) { + throw new Error("getAllDependencies did not return a Uint32Array"); + } + if (activeIds.toString() != exepected_dependencies.toString()) { + throw new Error(`${activeIds} != ${exepected_dependencies}`); + } +} + +const tests = [ + { + description: "Register states", + test: testStates, + }, +]; + +let exitCode = 0; +for (let test of tests) { + try { + await test.test(); + console.log("✓", test.description); + exitCode |= 0; + } + catch (error) { + console.log("x", test.description); + console.log(error); + exitCode |= 1; + } +} +process.exit(exitCode); diff --git a/Web/WebAssembly/post.js b/Web/WebAssembly/post.js new file mode 100644 index 000000000..c13443372 --- /dev/null +++ b/Web/WebAssembly/post.js @@ -0,0 +1 @@ +globalThis.createVTKWasmSceneManager = vtkWasmSceneManager; diff --git a/Web/WebAssembly/vtk.module b/Web/WebAssembly/vtk.module new file mode 100644 index 000000000..bf2a3ff7a --- /dev/null +++ b/Web/WebAssembly/vtk.module @@ -0,0 +1,17 @@ +NAME + VTK::WebAssembly +LIBRARY_NAME + vtkWebAssembly +SPDX_LICENSE_IDENTIFIER + BSD-3-Clause +SPDX_COPYRIGHT_TEXT + Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen +DEPENDS + VTK::SerializationManager +PRIVATE_DEPENDS + VTK::RenderingCore +OPTIONAL_DEPENDS + VTK::RenderingContextOpenGL2 + VTK::RenderingOpenGL2 + VTK::RenderingUI + VTK::RenderingVolumeOpenGL2 diff --git a/Web/WebAssembly/vtkWasmSceneManager.cxx b/Web/WebAssembly/vtkWasmSceneManager.cxx new file mode 100644 index 000000000..41858ee20 --- /dev/null +++ b/Web/WebAssembly/vtkWasmSceneManager.cxx @@ -0,0 +1,232 @@ +// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen +// SPDX-License-Identifier: BSD-3-Clause +#include "vtkWasmSceneManager.h" + +#include "vtkCallbackCommand.h" +#include "vtkCommand.h" +#include "vtkObjectFactory.h" + +#include "vtkRenderWindow.h" +#include "vtkRenderWindowInteractor.h" +#include "vtkRenderer.h" +#include "vtkWebAssemblyRenderWindowInteractor.h" + +// Init factories. +#ifdef VTK_MODULE_ENABLE_VTK_RenderingContextOpenGL2 +#include "vtkRenderingContextOpenGL2Module.h" +#endif +#ifdef VTK_MODULE_ENABLE_VTK_RenderingOpenGL2 +#include "vtkOpenGLPolyDataMapper.h" // needed to remove unused mapper, also includes vtkRenderingOpenGL2Module.h +#include "vtkWebAssemblyOpenGLRenderWindow.h" +#endif +#ifdef VTK_MODULE_ENABLE_VTK_RenderingUI +#include "vtkRenderingUIModule.h" +#endif +#ifdef VTK_MODULE_ENABLE_VTK_RenderingVolumeOpenGL2 +#include "vtkRenderingVolumeOpenGL2Module.h" +#endif + +VTK_ABI_NAMESPACE_BEGIN +//------------------------------------------------------------------------------- +vtkStandardNewMacro(vtkWasmSceneManager); + +//------------------------------------------------------------------------------- +vtkWasmSceneManager::vtkWasmSceneManager() = default; + +//------------------------------------------------------------------------------- +vtkWasmSceneManager::~vtkWasmSceneManager() = default; + +//------------------------------------------------------------------------------- +bool vtkWasmSceneManager::Initialize() +{ + bool result = this->Superclass::Initialize(); +#ifdef VTK_MODULE_ENABLE_VTK_RenderingOpenGL2 + // Remove the default vtkOpenGLPolyDataMapper as it is not used with wasm build. + /// get rid of serialization handler + this->Serializer->UnRegisterHandler(typeid(vtkOpenGLPolyDataMapper)); + /// get rid of de-serialization handler + this->Deserializer->UnRegisterHandler(typeid(vtkOpenGLPolyDataMapper)); + /// get rid of constructor + this->Deserializer->UnRegisterConstructor("vtkOpenGLPolyDataMapper"); +#endif + return result; +} + +//------------------------------------------------------------------------------- +void vtkWasmSceneManager::PrintSelf(ostream& os, vtkIndent indent) +{ + this->vtkObjectManager::PrintSelf(os, indent); +} + +//------------------------------------------------------------------------------- +bool vtkWasmSceneManager::SetSize(vtkTypeUInt32 identifier, int width, int height) +{ + auto object = this->GetObjectAtId(identifier); + if (auto renderWindow = vtkRenderWindow::SafeDownCast(object)) + { + if (auto iren = renderWindow->GetInteractor()) + { + iren->UpdateSize(width, height); + return true; + } + } + return false; +} + +//------------------------------------------------------------------------------- +bool vtkWasmSceneManager::Render(vtkTypeUInt32 identifier) +{ + auto object = this->GetObjectAtId(identifier); + if (auto renderWindow = vtkRenderWindow::SafeDownCast(object)) + { + renderWindow->Render(); + return true; + } + return false; +} + +//------------------------------------------------------------------------------- +bool vtkWasmSceneManager::ResetCamera(vtkTypeUInt32 identifier) +{ + auto object = this->GetObjectAtId(identifier); + if (auto renderer = vtkRenderer::SafeDownCast(object)) + { + renderer->ResetCamera(); + return true; + } + return false; +} + +//------------------------------------------------------------------------------- +bool vtkWasmSceneManager::StartEventLoop(vtkTypeUInt32 identifier) +{ + vtkRenderWindowInteractor::InteractorManagesTheEventLoop = false; + auto object = this->GetObjectAtId(identifier); + if (auto* renderWindow = vtkRenderWindow::SafeDownCast(object)) + { + if (auto* interactor = + vtkWebAssemblyRenderWindowInteractor::SafeDownCast(renderWindow->GetInteractor())) + { + if (auto* wasmGLWindow = vtkWebAssemblyOpenGLRenderWindow::SafeDownCast(renderWindow)) + { + // copy canvas selector from the render window to the interactor. + interactor->SetCanvasSelector(wasmGLWindow->GetCanvasSelector()); + std::cout << "Started event loop id=" << identifier + << ", interactor=" << interactor->GetObjectDescription() << '\n'; + interactor->Start(); + return true; + } + else + { + std::cerr << "Render window class " << renderWindow->GetClassName() + << " is not recognized!\n"; + } + } + else + { + std::cerr << "Interactor class " << renderWindow->GetClassName() << " is not recognized!\n"; + } + } + return false; +} + +//------------------------------------------------------------------------------- +bool vtkWasmSceneManager::StopEventLoop(vtkTypeUInt32 identifier) +{ + auto object = this->GetObjectAtId(identifier); + if (auto renderWindow = vtkRenderWindow::SafeDownCast(object)) + { + auto interactor = renderWindow->GetInteractor(); + std::cout << "Stopping event loop id=" << identifier + << ", interactor=" << interactor->GetObjectDescription() << '\n'; + interactor->TerminateApp(); + return true; + } + return false; +} + +namespace +{ +struct CallbackBridge +{ + vtkWasmSceneManager::ObserverCallbackF f; + vtkTypeUInt32 SenderId; +}; +} + +//------------------------------------------------------------------------------- +unsigned long vtkWasmSceneManager::AddObserver( + vtkTypeUInt32 identifier, std::string eventName, ObserverCallbackF callback) +{ + auto object = vtkObject::SafeDownCast(this->GetObjectAtId(identifier)); + if (object == nullptr) + { + return 0; + } + vtkNew callbackCmd; + callbackCmd->SetClientData(new CallbackBridge{ callback, identifier }); + callbackCmd->SetClientDataDeleteCallback( + [](void* clientData) + { + auto* bridge = reinterpret_cast(clientData); + delete bridge; + }); + callbackCmd->SetCallback( + [](vtkObject*, unsigned long eid, void* clientData, void*) + { + auto* bridge = reinterpret_cast(clientData); + bridge->f(bridge->SenderId, vtkCommand::GetStringFromEventId(eid)); + }); + return object->AddObserver(eventName.c_str(), callbackCmd); +} + +//------------------------------------------------------------------------------- +bool vtkWasmSceneManager::RemoveObserver(vtkTypeUInt32 identifier, unsigned long tag) +{ + + auto object = vtkObject::SafeDownCast(this->GetObjectAtId(identifier)); + if (object == nullptr) + { + return false; + } + object->RemoveObserver(tag); + return true; +} + +bool vtkWasmSceneManager::BindRenderWindow( + vtkTypeUInt32 renderWindowIdentifier, const char* canvasSelector) +{ + if (auto* renderWindow = + vtkRenderWindow::SafeDownCast(this->GetObjectAtId(renderWindowIdentifier))) + { + if (auto* wasmGLWindow = vtkWebAssemblyOpenGLRenderWindow::SafeDownCast(renderWindow)) + { + wasmGLWindow->SetCanvasSelector(canvasSelector); + if (auto* interactor = + vtkWebAssemblyRenderWindowInteractor::SafeDownCast(renderWindow->GetInteractor())) + { + interactor->SetCanvasSelector(canvasSelector); + return true; + } + else + { + std::cerr << "No interactor found for render window with identifier: " + << renderWindowIdentifier << '\n'; + return false; + } + } + else + { + std::cerr << "Render window class " << renderWindow->GetClassName() + << " is not recognized!\n"; + return false; + } + } + else + { + std::cerr << "No render window found with identifier: " << renderWindowIdentifier << '\n'; + return false; + } +} + +VTK_ABI_NAMESPACE_END diff --git a/Web/WebAssembly/vtkWasmSceneManager.h b/Web/WebAssembly/vtkWasmSceneManager.h new file mode 100644 index 000000000..9dd35c179 --- /dev/null +++ b/Web/WebAssembly/vtkWasmSceneManager.h @@ -0,0 +1,114 @@ +// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen +// SPDX-License-Identifier: BSD-3-Clause +/** + * @class vtkWasmSceneManager + * @brief vtkWasmSceneManager provides additional functionality that relates to a vtkRenderWindow + * and user interaction. + * + * `vtkWasmSceneManager` is a javascript wrapper of `vtkSceneManager` for managing VTK + * objects, specifically designed for webassembly (wasm). It extends + * functionality of `vtkObjectManager` for managing objects such as `vtkRenderWindow`, + * `vtkRenderWindowInteractor` and enables event-observers in webassembly + * visualization applications. + * + * @sa vtkObjectManager + */ +#ifndef vtkWasmSceneManager_h +#define vtkWasmSceneManager_h + +#include "vtkObjectManager.h" + +#include "vtkSerializationManagerModule.h" // for export macro + +VTK_ABI_NAMESPACE_BEGIN + +class VTKSERIALIZATIONMANAGER_EXPORT vtkWasmSceneManager : public vtkObjectManager +{ +public: + static vtkWasmSceneManager* New(); + vtkTypeMacro(vtkWasmSceneManager, vtkObjectManager); + void PrintSelf(ostream& os, vtkIndent indent) override; + + bool Initialize() override; + + /** + * Set the size of the `vtkRenderWindow` object at `identifier` to + * the supplied dimensions. + * + * Returns `true` if the object at `identifier` is a `vtkRenderWindow` + * with a `vtkRenderWindowInteractor` attached to it, + * `false` otherwise. + */ + bool SetSize(vtkTypeUInt32 identifier, int width, int height); + + /** + * Render the `vtkRenderWindow` object at `identifier`. + * + * Returns `true` if the object at `identifier` is a `vtkRenderWindow` + * `false` otherwise. + */ + bool Render(vtkTypeUInt32 identifier); + + /** + * Reset the active camera of the `vtkRenderer` object at `identifier`. + * + * Returns `true` if the object at `identifier` is a `vtkRenderer` + * `false` otherwise. + */ + bool ResetCamera(vtkTypeUInt32 identifier); + + /** + * Start event loop of the `vtkRenderWindowInteractor` object at `identifier`. + * + * Returns `true` if the object at `identifier` is a `vtkRenderWindowInteractor` + * `false` otherwise. + */ + bool StartEventLoop(vtkTypeUInt32 identifier); + + /** + * Stop event loop of the `vtkRenderWindowInteractor` object at `identifier`. + * + * Returns `true` if the object at `identifier` is a `vtkRenderWindowInteractor` + * `false` otherwise. + */ + bool StopEventLoop(vtkTypeUInt32 identifier); + + typedef void (*ObserverCallbackF)(vtkTypeUInt32, const char*); + + /** + * Observes `eventName` event emitted by an object registered at `identifier` + * and invokes `callback` with the `identifier` and `eventName` for every such emission. + * + * Returns the tag of an observer for `eventName`. You can use the tag in `RemoveObserver` + * to stop observing `eventName` event from the object at `identifier` + */ + unsigned long AddObserver( + vtkTypeUInt32 identifier, std::string eventName, ObserverCallbackF callback); + + /** + * Stop observing the object at `identifier`. + * Returns `true` if an object exists at `identifier`, + * `false` otherwise. + */ + bool RemoveObserver(vtkTypeUInt32 identifier, unsigned long tag); + + /** + * Bind a `vtkRenderWindow` object at `renderWindowIdentifier` to a canvas element with the + * specified `canvasSelector`. This allows the `vtkRenderWindow` to render its content onto the + * specified HTML canvas element in a web application. + * + * @param renderWindowIdentifier The identifier of the `vtkRenderWindow` object to bind. + * @param canvasSelector The ID of the HTML canvas element to bind the `vtkRenderWindow` to. + */ + bool BindRenderWindow(vtkTypeUInt32 renderWindowIdentifier, const char* canvasSelector); + +protected: + vtkWasmSceneManager(); + ~vtkWasmSceneManager() override; + +private: + vtkWasmSceneManager(const vtkWasmSceneManager&) = delete; + void operator=(const vtkWasmSceneManager&) = delete; +}; +VTK_ABI_NAMESPACE_END +#endif diff --git a/Web/WebAssembly/vtkWasmSceneManagerEmBinding.cxx b/Web/WebAssembly/vtkWasmSceneManagerEmBinding.cxx new file mode 100644 index 000000000..68fc1bf6d --- /dev/null +++ b/Web/WebAssembly/vtkWasmSceneManagerEmBinding.cxx @@ -0,0 +1,463 @@ +// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen +// SPDX-License-Identifier: BSD-3-Clause +#include +#include + +#include "vtkDataArrayRange.h" +#include "vtkTypeUInt8Array.h" +#include "vtkVersion.h" +#include "vtkWasmSceneManager.h" + +// clang-format off +#include "vtk_nlohmannjson.h" // for json +#include VTK_NLOHMANN_JSON(json_fwd.hpp) // for json +// clang-format on + +#include +#include + +namespace +{ + +#define CHECK_INIT \ + do \ + { \ + if (Manager == nullptr) \ + { \ + std::cerr << "Manager is null. Did you call forget to call initialize()?\n"; \ + } \ + } while (0) + +vtkWasmSceneManager* Manager = nullptr; + +std::map> SkippedClassProperties; + +using namespace emscripten; + +thread_local const val Uint8Array = val::global("Uint8Array"); +thread_local const val Uint32Array = val::global("Uint32Array"); +thread_local const val JSON = val::global("JSON"); + +//------------------------------------------------------------------------------- +bool initialize() +{ + Manager = vtkWasmSceneManager::New(); + return Manager->Initialize(); +} + +//------------------------------------------------------------------------------- +void finalize() +{ + CHECK_INIT; + Manager->UnRegister(nullptr); +} + +//------------------------------------------------------------------------------- +bool registerState(const std::string& state) +{ + CHECK_INIT; + auto stateJson = nlohmann::json::parse(state, nullptr, false); + if (stateJson.is_discarded()) + { + vtkErrorWithObjectMacro(Manager, << "Failed to parse state!"); + return false; + } + if (auto classNameIter = stateJson.find("ClassName"); classNameIter != stateJson.end()) + { + if (*classNameIter == "vtkOSOpenGLRenderWindow") + { + *classNameIter = "vtkWebAssemblyOpenGLRenderWindow"; + } + if (auto propertiesIter = SkippedClassProperties.find(*classNameIter); + propertiesIter != SkippedClassProperties.end()) + { + for (const auto& propertyName : propertiesIter->second) + { + stateJson.erase(propertyName); + } + } + } + return Manager->RegisterState(stateJson); +} + +//------------------------------------------------------------------------------- +bool registerState(val stateJavaScriptJSON) +{ + CHECK_INIT; + const auto stringified = JSON.call("stringify", stateJavaScriptJSON); + return ::registerState(stringified.as()); +} + +//------------------------------------------------------------------------------- +bool unRegisterState(vtkTypeUInt32 identifier) +{ + CHECK_INIT; + return Manager->UnRegisterState(identifier); +} + +//------------------------------------------------------------------------------- +val getState(vtkTypeUInt32 identifier) +{ + CHECK_INIT; + return JSON.call("parse", Manager->GetState(identifier)); +} + +//------------------------------------------------------------------------------- +void skipProperty(const std::string& className, const std::string& propertyName) +{ + SkippedClassProperties[className].insert(propertyName); +} + +//------------------------------------------------------------------------------- +void unSkipProperty(const std::string& className, const std::string& propertyName) +{ + SkippedClassProperties[className].erase(propertyName); +} + +//------------------------------------------------------------------------------- +bool unRegisterObject(vtkTypeUInt32 identifier) +{ + CHECK_INIT; + return Manager->UnRegisterObject(identifier); +} + +//------------------------------------------------------------------------------- +bool registerBlob(const std::string& hash, val jsArray) +{ + CHECK_INIT; + if (jsArray.instanceof (val::global("Uint8Array"))) + { + const vtkIdType l = jsArray["length"].as(); + auto blob = vtk::TakeSmartPointer(vtkTypeUInt8Array::New()); + blob->SetNumberOfValues(l); + auto blobRange = vtk::DataArrayValueRange(blob); + val memoryView{ typed_memory_view(static_cast(l), blobRange.data()) }; + memoryView.call("set", jsArray); + return Manager->RegisterBlob(hash, blob); + } + else + { + std::cerr << "Invalid type! Expects instanceof blob == Uint8Array" << std::endl; + return false; + } +} + +//------------------------------------------------------------------------------- +bool unRegisterBlob(const std::string& hash) +{ + CHECK_INIT; + return Manager->UnRegisterBlob(hash); +} + +//------------------------------------------------------------------------------- +val getBlob(const std::string& hash) +{ + CHECK_INIT; + const auto blob = Manager->GetBlob(hash); + val jsBlob = Uint8Array.new_(typed_memory_view(blob->GetNumberOfValues(), blob->GetPointer(0))); + return jsBlob; +} + +//------------------------------------------------------------------------------- +void pruneUnusedBlobs() +{ + CHECK_INIT; + Manager->PruneUnusedBlobs(); +} + +//------------------------------------------------------------------------------- +void clear() +{ + CHECK_INIT; + Manager->Clear(); +} + +//------------------------------------------------------------------------------- +val invoke(vtkTypeUInt32 identifier, const std::string& methodName, val args) +{ + CHECK_INIT; + const auto stringified = JSON.call("stringify", args); + return JSON.call( + "parse", Manager->Invoke(identifier, methodName, stringified.as())); +} + +//------------------------------------------------------------------------------- +val getAllDependencies(vtkTypeUInt32 identifier) +{ + CHECK_INIT; + const auto ids = Manager->GetAllDependencies(identifier); + val jsIds = Uint32Array.new_(typed_memory_view(ids.size(), ids.data())); + return jsIds; +} +//------------------------------------------------------------------------------- +std::size_t getTotalBlobMemoryUsage() +{ + CHECK_INIT; + return ::Manager->GetTotalBlobMemoryUsage(); +} + +//------------------------------------------------------------------------------- +std::size_t getTotalVTKDataObjectMemoryUsage() +{ + CHECK_INIT; + return ::Manager->GetTotalVTKDataObjectMemoryUsage(); +} + +//------------------------------------------------------------------------------- +void updateObjectsFromStates() +{ + CHECK_INIT; + Manager->UpdateObjectsFromStates(); +} + +//------------------------------------------------------------------------------- +void updateStatesFromObjects() +{ + CHECK_INIT; + Manager->UpdateStatesFromObjects(); +} + +//------------------------------------------------------------------------------- +void updateObjectFromState(const std::string& state) +{ + CHECK_INIT; + auto stateJson = nlohmann::json::parse(state, nullptr, false); + if (stateJson.is_discarded()) + { + vtkErrorWithObjectMacro(Manager, << "Failed to parse state!"); + } + else if (auto idIter = stateJson.find("Id"); idIter != stateJson.end()) + { + if (auto classNameIter = stateJson.find("ClassName"); classNameIter != stateJson.end()) + { + if (*classNameIter == "vtkOSOpenGLRenderWindow") + { + *classNameIter = "vtkWebAssemblyOpenGLRenderWindow"; + } + } + if (auto objectAtId = Manager->GetObjectAtId(*idIter)) + { + const std::string className = objectAtId->GetClassName(); + if (auto propertiesIter = SkippedClassProperties.find(className); + propertiesIter != SkippedClassProperties.end()) + { + for (const auto& propertyName : propertiesIter->second) + { + stateJson.erase(propertyName); + } + } + } + } + Manager->UpdateObjectFromState(stateJson); +} + +//------------------------------------------------------------------------------- +void updateObjectFromState(val stateJavaScriptJSON) +{ + CHECK_INIT; + const auto stringified = JSON.call("stringify", stateJavaScriptJSON); + updateObjectFromState(stringified.as()); +} + +//------------------------------------------------------------------------------- +void updateStateFromObject(vtkTypeUInt32 identifier) +{ + CHECK_INIT; + Manager->UpdateStateFromObject(identifier); +} + +//------------------------------------------------------------------------------- +bool setSize(vtkTypeUInt32 identifier, int width, int height) +{ + CHECK_INIT; + return Manager->SetSize(identifier, width, height); +} + +//------------------------------------------------------------------------------- +bool render(vtkTypeUInt32 identifier) +{ + CHECK_INIT; + return Manager->Render(identifier); +} + +//------------------------------------------------------------------------------- +bool resetCamera(vtkTypeUInt32 identifier) +{ + CHECK_INIT; + return Manager->ResetCamera(identifier); +} + +//------------------------------------------------------------------------------- +bool startEventLoop(vtkTypeUInt32 identifier) +{ + CHECK_INIT; + return Manager->StartEventLoop(identifier); +} + +//------------------------------------------------------------------------------- +bool stopEventLoop(vtkTypeUInt32 identifier) +{ + CHECK_INIT; + return Manager->StopEventLoop(identifier); +} + +//------------------------------------------------------------------------------- +unsigned long addObserver(vtkTypeUInt32 identifier, std::string eventName, val jsFunc) +{ + CHECK_INIT; + int fp = val::module_property("addFunction")(jsFunc, std::string("vii")).as(); + auto callback = reinterpret_cast(fp); + return Manager->AddObserver(identifier, eventName, callback); +} + +//------------------------------------------------------------------------------- +bool removeObserver(vtkTypeUInt32 identifier, unsigned long tag) +{ + CHECK_INIT; + return Manager->RemoveObserver(identifier, tag); +} + +//------------------------------------------------------------------------------- +bool bindRenderWindow(vtkTypeUInt32 renderWindowIdentifier, const std::string& canvasSelector) +{ + CHECK_INIT; + return Manager->BindRenderWindow(renderWindowIdentifier, canvasSelector.c_str()); +} + +//------------------------------------------------------------------------------- +void import(const std::string& stateFileName, const std::string& blobFileName) +{ + CHECK_INIT; + Manager->Import(stateFileName, blobFileName); +} + +//------------------------------------------------------------------------------- +void printSceneManagerInformation() +{ + CHECK_INIT; + Manager->Print(std::cout); +} + +//------------------------------------------------------------------------------- +void setDeserializerLogVerbosity(const std::string& verbosityStr) +{ + CHECK_INIT; + const auto verbosity = vtkLogger::ConvertToVerbosity(verbosityStr.c_str()); + if (verbosity != vtkLogger::VERBOSITY_INVALID) + { + Manager->GetDeserializer()->SetDeserializerLogVerbosity(verbosity); + } +} + +//------------------------------------------------------------------------------- +void setInvokerLogVerbosity(const std::string& verbosityStr) +{ + CHECK_INIT; + const auto verbosity = vtkLogger::ConvertToVerbosity(verbosityStr.c_str()); + if (verbosity != vtkLogger::VERBOSITY_INVALID) + { + Manager->GetInvoker()->SetInvokerLogVerbosity(verbosity); + } +} + +//------------------------------------------------------------------------------- +void setObjectManagerLogVerbosity(const std::string& verbosityStr) +{ + CHECK_INIT; + const auto verbosity = vtkLogger::ConvertToVerbosity(verbosityStr.c_str()); + if (verbosity != vtkLogger::VERBOSITY_INVALID) + { + Manager->SetObjectManagerLogVerbosity(verbosity); + } +} + +//------------------------------------------------------------------------------- +void setSerializerLogVerbosity(const std::string& verbosityStr) +{ + CHECK_INIT; + const auto verbosity = vtkLogger::ConvertToVerbosity(verbosityStr.c_str()); + if (verbosity != vtkLogger::VERBOSITY_INVALID) + { + Manager->GetSerializer()->SetSerializerLogVerbosity(verbosity); + } +} + +//------------------------------------------------------------------------------- +std::string getVTKVersion() +{ + return vtkVersion::GetVTKVersion(); +} + +//------------------------------------------------------------------------------- +std::string getVTKVersionFull() +{ + return vtkVersion::GetVTKVersionFull(); +} + +} // namespace + +EMSCRIPTEN_BINDINGS(vtkWasmSceneManager) +{ + function("initialize", ::initialize); + function("finalize", ::finalize); + + function("registerState", select_overload(::registerState)); + function("registerStateJSON", select_overload(::registerState)); + function("unRegisterState", ::unRegisterState); + function("getState", ::getState); + function("skipProperty", ::skipProperty); + function("unSkipProperty", ::unSkipProperty); + + function("unRegisterObject", ::unRegisterObject); + + function("registerBlob", ::registerBlob); + function("unRegisterBlob", ::unRegisterBlob); + function("getBlob", ::getBlob); + function("pruneUnusedBlobs", ::pruneUnusedBlobs); + + function("clear", ::clear); + function("invoke", ::invoke); + + function("getAllDependencies", ::getAllDependencies); + + function("getTotalBlobMemoryUsage", ::getTotalBlobMemoryUsage); + function("getTotalVTKDataObjectMemoryUsage", ::getTotalVTKDataObjectMemoryUsage); + + function("updateObjectsFromStates", ::updateObjectsFromStates); + function("updateStatesFromObjects", ::updateStatesFromObjects); + + function( + "updateObjectFromState", select_overload(::updateObjectFromState)); + function("updateObjectFromStateJSON", select_overload(::updateObjectFromState)); + function("updateStateFromObject", ::updateStateFromObject); + + function("setSize", ::setSize); + function("render", ::render); + function("resetCamera", ::resetCamera); + + function("startEventLoop", ::startEventLoop); + function("stopEventLoop", ::stopEventLoop); + + function("addObserver", ::addObserver); + function("removeObserver", ::removeObserver); + + function("bindRenderWindow", ::bindRenderWindow); + + function("import", ::import); + + // debugging + function("printSceneManagerInformation", ::printSceneManagerInformation); + // accepts JS strings like "INFO", "WARNING", "TRACE", "ERROR" + function("setDeserializerLogVerbosity", ::setDeserializerLogVerbosity); + function("setInvokerLogVerbosity", ::setInvokerLogVerbosity); + function("setObjectManagerLogVerbosity", ::setObjectManagerLogVerbosity); + function("setSerializerLogVerbosity", ::setSerializerLogVerbosity); + + function("getVTKVersion", ::getVTKVersion); + function("getVTKVersionFull", ::getVTKVersionFull); +} + +int main() +{ + return 0; +} diff --git a/Web/WebGLExporter/CMakeLists.txt b/Web/WebGLExporter/CMakeLists.txt new file mode 100644 index 000000000..0edb3c268 --- /dev/null +++ b/Web/WebGLExporter/CMakeLists.txt @@ -0,0 +1,42 @@ +# The exporter will behave as any other ParaView exporter (VRML, X3D, POV...) +# but will generate several types of files. The main one is the scene graph +# description define as a JSON object with all the corresponding binary+base64 +# pieces that come along with it. But also with it come a single standalone HTML +# file that can directly be used to see the data in a browser without any plugin. +# +# This code base should be cleaned up to follow VTK standard and even be +# integrated into VTK itself. But for now it is provided as is. + +set(classes + vtkPVWebGLExporter + vtkWebGLDataSet + vtkWebGLExporter + vtkWebGLObject + vtkWebGLPolyData + vtkWebGLWidget) + +set(javascript_files + webglRenderer.js + glMatrix.js) + +set(sources) +set(private_headers) + +foreach (javascript_file IN LISTS javascript_files) + vtk_encode_string( + INPUT "${javascript_file}" + EXPORT_HEADER "vtkWebGLExporterModule.h" + EXPORT_SYMBOL "VTKWEBGLEXPORTER_NO_EXPORT" + HEADER_OUTPUT header + SOURCE_OUTPUT source) + list(APPEND sources + ${source}) + list(APPEND private_headers + ${header}) +endforeach () + +vtk_module_add_module(VTK::WebGLExporter + CLASSES ${classes} + SOURCES ${sources} + PRIVATE_HEADERS ${private_headers}) +vtk_add_test_mangling(VTK::WebGLExporter) diff --git a/Web/WebGLExporter/glMatrix.js b/Web/WebGLExporter/glMatrix.js new file mode 100644 index 000000000..4e4a830d1 --- /dev/null +++ b/Web/WebGLExporter/glMatrix.js @@ -0,0 +1,32 @@ +// glMatrix v0.9.5 +glMatrixArrayType=typeof Float32Array!="undefined"?Float32Array:typeof WebGLFloatArray!="undefined"?WebGLFloatArray:Array;var vec3={};vec3.create=function(a){var b=new glMatrixArrayType(3);if(a){b[0]=a[0];b[1]=a[1];b[2]=a[2]}return b};vec3.set=function(a,b){b[0]=a[0];b[1]=a[1];b[2]=a[2];return b};vec3.add=function(a,b,c){if(!c||a==c){a[0]+=b[0];a[1]+=b[1];a[2]+=b[2];return a}c[0]=a[0]+b[0];c[1]=a[1]+b[1];c[2]=a[2]+b[2];return c}; +vec3.subtract=function(a,b,c){if(!c||a==c){a[0]-=b[0];a[1]-=b[1];a[2]-=b[2];return a}c[0]=a[0]-b[0];c[1]=a[1]-b[1];c[2]=a[2]-b[2];return c};vec3.negate=function(a,b){b||(b=a);b[0]=-a[0];b[1]=-a[1];b[2]=-a[2];return b};vec3.scale=function(a,b,c){if(!c||a==c){a[0]*=b;a[1]*=b;a[2]*=b;return a}c[0]=a[0]*b;c[1]=a[1]*b;c[2]=a[2]*b;return c}; +vec3.normalize=function(a,b){b||(b=a);var c=a[0],d=a[1],e=a[2],g=Math.sqrt(c*c+d*d+e*e);if(g){if(g==1){b[0]=c;b[1]=d;b[2]=e;return b}}else{b[0]=0;b[1]=0;b[2]=0;return b}g=1/g;b[0]=c*g;b[1]=d*g;b[2]=e*g;return b};vec3.cross=function(a,b,c){c||(c=a);var d=a[0],e=a[1];a=a[2];var g=b[0],f=b[1];b=b[2];c[0]=e*b-a*f;c[1]=a*g-d*b;c[2]=d*f-e*g;return c};vec3.length=function(a){var b=a[0],c=a[1];a=a[2];return Math.sqrt(b*b+c*c+a*a)};vec3.dot=function(a,b){return a[0]*b[0]+a[1]*b[1]+a[2]*b[2]}; +vec3.direction=function(a,b,c){c||(c=a);var d=a[0]-b[0],e=a[1]-b[1];a=a[2]-b[2];b=Math.sqrt(d*d+e*e+a*a);if(!b){c[0]=0;c[1]=0;c[2]=0;return c}b=1/b;c[0]=d*b;c[1]=e*b;c[2]=a*b;return c};vec3.lerp=function(a,b,c,d){d||(d=a);d[0]=a[0]+c*(b[0]-a[0]);d[1]=a[1]+c*(b[1]-a[1]);d[2]=a[2]+c*(b[2]-a[2]);return d};vec3.str=function(a){return"["+a[0]+", "+a[1]+", "+a[2]+"]"};var mat3={}; +mat3.create=function(a){var b=new glMatrixArrayType(9);if(a){b[0]=a[0];b[1]=a[1];b[2]=a[2];b[3]=a[3];b[4]=a[4];b[5]=a[5];b[6]=a[6];b[7]=a[7];b[8]=a[8];b[9]=a[9]}return b};mat3.set=function(a,b){b[0]=a[0];b[1]=a[1];b[2]=a[2];b[3]=a[3];b[4]=a[4];b[5]=a[5];b[6]=a[6];b[7]=a[7];b[8]=a[8];return b};mat3.identity=function(a){a[0]=1;a[1]=0;a[2]=0;a[3]=0;a[4]=1;a[5]=0;a[6]=0;a[7]=0;a[8]=1;return a}; +mat3.transpose=function(a,b){if(!b||a==b){var c=a[1],d=a[2],e=a[5];a[1]=a[3];a[2]=a[6];a[3]=c;a[5]=a[7];a[6]=d;a[7]=e;return a}b[0]=a[0];b[1]=a[3];b[2]=a[6];b[3]=a[1];b[4]=a[4];b[5]=a[7];b[6]=a[2];b[7]=a[5];b[8]=a[8];return b};mat3.toMat4=function(a,b){b||(b=mat4.create());b[0]=a[0];b[1]=a[1];b[2]=a[2];b[3]=0;b[4]=a[3];b[5]=a[4];b[6]=a[5];b[7]=0;b[8]=a[6];b[9]=a[7];b[10]=a[8];b[11]=0;b[12]=0;b[13]=0;b[14]=0;b[15]=1;return b}; +mat3.str=function(a){return"["+a[0]+", "+a[1]+", "+a[2]+", "+a[3]+", "+a[4]+", "+a[5]+", "+a[6]+", "+a[7]+", "+a[8]+"]"};var mat4={};mat4.create=function(a){var b=new glMatrixArrayType(16);if(a){b[0]=a[0];b[1]=a[1];b[2]=a[2];b[3]=a[3];b[4]=a[4];b[5]=a[5];b[6]=a[6];b[7]=a[7];b[8]=a[8];b[9]=a[9];b[10]=a[10];b[11]=a[11];b[12]=a[12];b[13]=a[13];b[14]=a[14];b[15]=a[15]}return b}; +mat4.set=function(a,b){b[0]=a[0];b[1]=a[1];b[2]=a[2];b[3]=a[3];b[4]=a[4];b[5]=a[5];b[6]=a[6];b[7]=a[7];b[8]=a[8];b[9]=a[9];b[10]=a[10];b[11]=a[11];b[12]=a[12];b[13]=a[13];b[14]=a[14];b[15]=a[15];return b};mat4.identity=function(a){a[0]=1;a[1]=0;a[2]=0;a[3]=0;a[4]=0;a[5]=1;a[6]=0;a[7]=0;a[8]=0;a[9]=0;a[10]=1;a[11]=0;a[12]=0;a[13]=0;a[14]=0;a[15]=1;return a}; +mat4.transpose=function(a,b){if(!b||a==b){var c=a[1],d=a[2],e=a[3],g=a[6],f=a[7],h=a[11];a[1]=a[4];a[2]=a[8];a[3]=a[12];a[4]=c;a[6]=a[9];a[7]=a[13];a[8]=d;a[9]=g;a[11]=a[14];a[12]=e;a[13]=f;a[14]=h;return a}b[0]=a[0];b[1]=a[4];b[2]=a[8];b[3]=a[12];b[4]=a[1];b[5]=a[5];b[6]=a[9];b[7]=a[13];b[8]=a[2];b[9]=a[6];b[10]=a[10];b[11]=a[14];b[12]=a[3];b[13]=a[7];b[14]=a[11];b[15]=a[15];return b}; +mat4.determinant=function(a){var b=a[0],c=a[1],d=a[2],e=a[3],g=a[4],f=a[5],h=a[6],i=a[7],j=a[8],k=a[9],l=a[10],o=a[11],m=a[12],n=a[13],p=a[14];a=a[15];return m*k*h*e-j*n*h*e-m*f*l*e+g*n*l*e+j*f*p*e-g*k*p*e-m*k*d*i+j*n*d*i+m*c*l*i-b*n*l*i-j*c*p*i+b*k*p*i+m*f*d*o-g*n*d*o-m*c*h*o+b*n*h*o+g*c*p*o-b*f*p*o-j*f*d*a+g*k*d*a+j*c*h*a-b*k*h*a-g*c*l*a+b*f*l*a}; +mat4.inverse=function(a,b){b||(b=a);var c=a[0],d=a[1],e=a[2],g=a[3],f=a[4],h=a[5],i=a[6],j=a[7],k=a[8],l=a[9],o=a[10],m=a[11],n=a[12],p=a[13],r=a[14],s=a[15],A=c*h-d*f,B=c*i-e*f,t=c*j-g*f,u=d*i-e*h,v=d*j-g*h,w=e*j-g*i,x=k*p-l*n,y=k*r-o*n,z=k*s-m*n,C=l*r-o*p,D=l*s-m*p,E=o*s-m*r,q=1/(A*E-B*D+t*C+u*z-v*y+w*x);b[0]=(h*E-i*D+j*C)*q;b[1]=(-d*E+e*D-g*C)*q;b[2]=(p*w-r*v+s*u)*q;b[3]=(-l*w+o*v-m*u)*q;b[4]=(-f*E+i*z-j*y)*q;b[5]=(c*E-e*z+g*y)*q;b[6]=(-n*w+r*t-s*B)*q;b[7]=(k*w-o*t+m*B)*q;b[8]=(f*D-h*z+j*x)*q; +b[9]=(-c*D+d*z-g*x)*q;b[10]=(n*v-p*t+s*A)*q;b[11]=(-k*v+l*t-m*A)*q;b[12]=(-f*C+h*y-i*x)*q;b[13]=(c*C-d*y+e*x)*q;b[14]=(-n*u+p*B-r*A)*q;b[15]=(k*u-l*B+o*A)*q;return b};mat4.toRotationMat=function(a,b){b||(b=mat4.create());b[0]=a[0];b[1]=a[1];b[2]=a[2];b[3]=a[3];b[4]=a[4];b[5]=a[5];b[6]=a[6];b[7]=a[7];b[8]=a[8];b[9]=a[9];b[10]=a[10];b[11]=a[11];b[12]=0;b[13]=0;b[14]=0;b[15]=1;return b}; +mat4.toMat3=function(a,b){b||(b=mat3.create());b[0]=a[0];b[1]=a[1];b[2]=a[2];b[3]=a[4];b[4]=a[5];b[5]=a[6];b[6]=a[8];b[7]=a[9];b[8]=a[10];return b};mat4.toInverseMat3=function(a,b){var c=a[0],d=a[1],e=a[2],g=a[4],f=a[5],h=a[6],i=a[8],j=a[9],k=a[10],l=k*f-h*j,o=-k*g+h*i,m=j*g-f*i,n=c*l+d*o+e*m;if(!n)return null;n=1/n;b||(b=mat3.create());b[0]=l*n;b[1]=(-k*d+e*j)*n;b[2]=(h*d-e*f)*n;b[3]=o*n;b[4]=(k*c-e*i)*n;b[5]=(-h*c+e*g)*n;b[6]=m*n;b[7]=(-j*c+d*i)*n;b[8]=(f*c-d*g)*n;return b}; +mat4.multiply=function(a,b,c){c||(c=a);var d=a[0],e=a[1],g=a[2],f=a[3],h=a[4],i=a[5],j=a[6],k=a[7],l=a[8],o=a[9],m=a[10],n=a[11],p=a[12],r=a[13],s=a[14];a=a[15];var A=b[0],B=b[1],t=b[2],u=b[3],v=b[4],w=b[5],x=b[6],y=b[7],z=b[8],C=b[9],D=b[10],E=b[11],q=b[12],F=b[13],G=b[14];b=b[15];c[0]=A*d+B*h+t*l+u*p;c[1]=A*e+B*i+t*o+u*r;c[2]=A*g+B*j+t*m+u*s;c[3]=A*f+B*k+t*n+u*a;c[4]=v*d+w*h+x*l+y*p;c[5]=v*e+w*i+x*o+y*r;c[6]=v*g+w*j+x*m+y*s;c[7]=v*f+w*k+x*n+y*a;c[8]=z*d+C*h+D*l+E*p;c[9]=z*e+C*i+D*o+E*r;c[10]=z* +g+C*j+D*m+E*s;c[11]=z*f+C*k+D*n+E*a;c[12]=q*d+F*h+G*l+b*p;c[13]=q*e+F*i+G*o+b*r;c[14]=q*g+F*j+G*m+b*s;c[15]=q*f+F*k+G*n+b*a;return c};mat4.multiplyVec3=function(a,b,c){c||(c=b);var d=b[0],e=b[1];b=b[2];c[0]=a[0]*d+a[4]*e+a[8]*b+a[12];c[1]=a[1]*d+a[5]*e+a[9]*b+a[13];c[2]=a[2]*d+a[6]*e+a[10]*b+a[14];return c}; +mat4.multiplyVec4=function(a,b,c){c||(c=b);var d=b[0],e=b[1],g=b[2];b=b[3];c[0]=a[0]*d+a[4]*e+a[8]*g+a[12]*b;c[1]=a[1]*d+a[5]*e+a[9]*g+a[13]*b;c[2]=a[2]*d+a[6]*e+a[10]*g+a[14]*b;c[3]=a[3]*d+a[7]*e+a[11]*g+a[15]*b;return c}; +mat4.translate=function(a,b,c){var d=b[0],e=b[1];b=b[2];if(!c||a==c){a[12]=a[0]*d+a[4]*e+a[8]*b+a[12];a[13]=a[1]*d+a[5]*e+a[9]*b+a[13];a[14]=a[2]*d+a[6]*e+a[10]*b+a[14];a[15]=a[3]*d+a[7]*e+a[11]*b+a[15];return a}var g=a[0],f=a[1],h=a[2],i=a[3],j=a[4],k=a[5],l=a[6],o=a[7],m=a[8],n=a[9],p=a[10],r=a[11];c[0]=g;c[1]=f;c[2]=h;c[3]=i;c[4]=j;c[5]=k;c[6]=l;c[7]=o;c[8]=m;c[9]=n;c[10]=p;c[11]=r;c[12]=g*d+j*e+m*b+a[12];c[13]=f*d+k*e+n*b+a[13];c[14]=h*d+l*e+p*b+a[14];c[15]=i*d+o*e+r*b+a[15];return c}; +mat4.scale=function(a,b,c){var d=b[0],e=b[1];b=b[2];if(!c||a==c){a[0]*=d;a[1]*=d;a[2]*=d;a[3]*=d;a[4]*=e;a[5]*=e;a[6]*=e;a[7]*=e;a[8]*=b;a[9]*=b;a[10]*=b;a[11]*=b;return a}c[0]=a[0]*d;c[1]=a[1]*d;c[2]=a[2]*d;c[3]=a[3]*d;c[4]=a[4]*e;c[5]=a[5]*e;c[6]=a[6]*e;c[7]=a[7]*e;c[8]=a[8]*b;c[9]=a[9]*b;c[10]=a[10]*b;c[11]=a[11]*b;c[12]=a[12];c[13]=a[13];c[14]=a[14];c[15]=a[15];return c}; +mat4.rotate=function(a,b,c,d){var e=c[0],g=c[1];c=c[2];var f=Math.sqrt(e*e+g*g+c*c);if(!f)return null;if(f!=1){f=1/f;e*=f;g*=f;c*=f}var h=Math.sin(b),i=Math.cos(b),j=1-i;b=a[0];f=a[1];var k=a[2],l=a[3],o=a[4],m=a[5],n=a[6],p=a[7],r=a[8],s=a[9],A=a[10],B=a[11],t=e*e*j+i,u=g*e*j+c*h,v=c*e*j-g*h,w=e*g*j-c*h,x=g*g*j+i,y=c*g*j+e*h,z=e*c*j+g*h;e=g*c*j-e*h;g=c*c*j+i;if(d){if(a!=d){d[12]=a[12];d[13]=a[13];d[14]=a[14];d[15]=a[15]}}else d=a;d[0]=b*t+o*u+r*v;d[1]=f*t+m*u+s*v;d[2]=k*t+n*u+A*v;d[3]=l*t+p*u+B* +v;d[4]=b*w+o*x+r*y;d[5]=f*w+m*x+s*y;d[6]=k*w+n*x+A*y;d[7]=l*w+p*x+B*y;d[8]=b*z+o*e+r*g;d[9]=f*z+m*e+s*g;d[10]=k*z+n*e+A*g;d[11]=l*z+p*e+B*g;return d};mat4.rotateX=function(a,b,c){var d=Math.sin(b);b=Math.cos(b);var e=a[4],g=a[5],f=a[6],h=a[7],i=a[8],j=a[9],k=a[10],l=a[11];if(c){if(a!=c){c[0]=a[0];c[1]=a[1];c[2]=a[2];c[3]=a[3];c[12]=a[12];c[13]=a[13];c[14]=a[14];c[15]=a[15]}}else c=a;c[4]=e*b+i*d;c[5]=g*b+j*d;c[6]=f*b+k*d;c[7]=h*b+l*d;c[8]=e*-d+i*b;c[9]=g*-d+j*b;c[10]=f*-d+k*b;c[11]=h*-d+l*b;return c}; +mat4.rotateY=function(a,b,c){var d=Math.sin(b);b=Math.cos(b);var e=a[0],g=a[1],f=a[2],h=a[3],i=a[8],j=a[9],k=a[10],l=a[11];if(c){if(a!=c){c[4]=a[4];c[5]=a[5];c[6]=a[6];c[7]=a[7];c[12]=a[12];c[13]=a[13];c[14]=a[14];c[15]=a[15]}}else c=a;c[0]=e*b+i*-d;c[1]=g*b+j*-d;c[2]=f*b+k*-d;c[3]=h*b+l*-d;c[8]=e*d+i*b;c[9]=g*d+j*b;c[10]=f*d+k*b;c[11]=h*d+l*b;return c}; +mat4.rotateZ=function(a,b,c){var d=Math.sin(b);b=Math.cos(b);var e=a[0],g=a[1],f=a[2],h=a[3],i=a[4],j=a[5],k=a[6],l=a[7];if(c){if(a!=c){c[8]=a[8];c[9]=a[9];c[10]=a[10];c[11]=a[11];c[12]=a[12];c[13]=a[13];c[14]=a[14];c[15]=a[15]}}else c=a;c[0]=e*b+i*d;c[1]=g*b+j*d;c[2]=f*b+k*d;c[3]=h*b+l*d;c[4]=e*-d+i*b;c[5]=g*-d+j*b;c[6]=f*-d+k*b;c[7]=h*-d+l*b;return c}; +mat4.frustum=function(a,b,c,d,e,g,f){f||(f=mat4.create());var h=b-a,i=d-c,j=g-e;f[0]=e*2/h;f[1]=0;f[2]=0;f[3]=0;f[4]=0;f[5]=e*2/i;f[6]=0;f[7]=0;f[8]=(b+a)/h;f[9]=(d+c)/i;f[10]=-(g+e)/j;f[11]=-1;f[12]=0;f[13]=0;f[14]=-(g*e*2)/j;f[15]=0;return f};mat4.perspective=function(a,b,c,d,e){a=c*Math.tan(a*Math.PI/360);b=a*b;return mat4.frustum(-b,b,-a,a,c,d,e)}; +mat4.ortho=function(a,b,c,d,e,g,f){f||(f=mat4.create());var h=b-a,i=d-c,j=g-e;f[0]=2/h;f[1]=0;f[2]=0;f[3]=0;f[4]=0;f[5]=2/i;f[6]=0;f[7]=0;f[8]=0;f[9]=0;f[10]=-2/j;f[11]=0;f[12]=-(a+b)/h;f[13]=-(d+c)/i;f[14]=-(g+e)/j;f[15]=1;return f}; +mat4.lookAt=function(a,b,c,d){d||(d=mat4.create());var e=a[0],g=a[1];a=a[2];var f=c[0],h=c[1],i=c[2];c=b[1];var j=b[2];if(e==b[0]&&g==c&&a==j)return mat4.identity(d);var k,l,o,m;c=e-b[0];j=g-b[1];b=a-b[2];m=1/Math.sqrt(c*c+j*j+b*b);c*=m;j*=m;b*=m;k=h*b-i*j;i=i*c-f*b;f=f*j-h*c;if(m=Math.sqrt(k*k+i*i+f*f)){m=1/m;k*=m;i*=m;f*=m}else f=i=k=0;h=j*f-b*i;l=b*k-c*f;o=c*i-j*k;if(m=Math.sqrt(h*h+l*l+o*o)){m=1/m;h*=m;l*=m;o*=m}else o=l=h=0;d[0]=k;d[1]=h;d[2]=c;d[3]=0;d[4]=i;d[5]=l;d[6]=j;d[7]=0;d[8]=f;d[9]= +o;d[10]=b;d[11]=0;d[12]=-(k*e+i*g+f*a);d[13]=-(h*e+l*g+o*a);d[14]=-(c*e+j*g+b*a);d[15]=1;return d};mat4.str=function(a){return"["+a[0]+", "+a[1]+", "+a[2]+", "+a[3]+", "+a[4]+", "+a[5]+", "+a[6]+", "+a[7]+", "+a[8]+", "+a[9]+", "+a[10]+", "+a[11]+", "+a[12]+", "+a[13]+", "+a[14]+", "+a[15]+"]"};quat4={};quat4.create=function(a){var b=new glMatrixArrayType(4);if(a){b[0]=a[0];b[1]=a[1];b[2]=a[2];b[3]=a[3]}return b};quat4.set=function(a,b){b[0]=a[0];b[1]=a[1];b[2]=a[2];b[3]=a[3];return b}; +quat4.calculateW=function(a,b){var c=a[0],d=a[1],e=a[2];if(!b||a==b){a[3]=-Math.sqrt(Math.abs(1-c*c-d*d-e*e));return a}b[0]=c;b[1]=d;b[2]=e;b[3]=-Math.sqrt(Math.abs(1-c*c-d*d-e*e));return b};quat4.inverse=function(a,b){if(!b||a==b){a[0]*=1;a[1]*=1;a[2]*=1;return a}b[0]=-a[0];b[1]=-a[1];b[2]=-a[2];b[3]=a[3];return b};quat4.length=function(a){var b=a[0],c=a[1],d=a[2];a=a[3];return Math.sqrt(b*b+c*c+d*d+a*a)}; +quat4.normalize=function(a,b){b||(b=a);var c=a[0],d=a[1],e=a[2],g=a[3],f=Math.sqrt(c*c+d*d+e*e+g*g);if(f==0){b[0]=0;b[1]=0;b[2]=0;b[3]=0;return b}f=1/f;b[0]=c*f;b[1]=d*f;b[2]=e*f;b[3]=g*f;return b};quat4.multiply=function(a,b,c){c||(c=a);var d=a[0],e=a[1],g=a[2];a=a[3];var f=b[0],h=b[1],i=b[2];b=b[3];c[0]=d*b+a*f+e*i-g*h;c[1]=e*b+a*h+g*f-d*i;c[2]=g*b+a*i+d*h-e*f;c[3]=a*b-d*f-e*h-g*i;return c}; +quat4.multiplyVec3=function(a,b,c){c||(c=b);var d=b[0],e=b[1],g=b[2];b=a[0];var f=a[1],h=a[2];a=a[3];var i=a*d+f*g-h*e,j=a*e+h*d-b*g,k=a*g+b*e-f*d;d=-b*d-f*e-h*g;c[0]=i*a+d*-b+j*-h-k*-f;c[1]=j*a+d*-f+k*-b-i*-h;c[2]=k*a+d*-h+i*-f-j*-b;return c};quat4.toMat3=function(a,b){b||(b=mat3.create());var c=a[0],d=a[1],e=a[2],g=a[3],f=c+c,h=d+d,i=e+e,j=c*f,k=c*h;c=c*i;var l=d*h;d=d*i;e=e*i;f=g*f;h=g*h;g=g*i;b[0]=1-(l+e);b[1]=k-g;b[2]=c+h;b[3]=k+g;b[4]=1-(j+e);b[5]=d-f;b[6]=c-h;b[7]=d+f;b[8]=1-(j+l);return b}; +quat4.toMat4=function(a,b){b||(b=mat4.create());var c=a[0],d=a[1],e=a[2],g=a[3],f=c+c,h=d+d,i=e+e,j=c*f,k=c*h;c=c*i;var l=d*h;d=d*i;e=e*i;f=g*f;h=g*h;g=g*i;b[0]=1-(l+e);b[1]=k-g;b[2]=c+h;b[3]=0;b[4]=k+g;b[5]=1-(j+e);b[6]=d-f;b[7]=0;b[8]=c-h;b[9]=d+f;b[10]=1-(j+l);b[11]=0;b[12]=0;b[13]=0;b[14]=0;b[15]=1;return b};quat4.slerp=function(a,b,c,d){d||(d=a);var e=c;if(a[0]*b[0]+a[1]*b[1]+a[2]*b[2]+a[3]*b[3]<0)e=-1*c;d[0]=1-c*a[0]+e*b[0];d[1]=1-c*a[1]+e*b[1];d[2]=1-c*a[2]+e*b[2];d[3]=1-c*a[3]+e*b[3];return d}; +quat4.str=function(a){return"["+a[0]+", "+a[1]+", "+a[2]+", "+a[3]+"]"}; diff --git a/Web/WebGLExporter/vtk.module b/Web/WebGLExporter/vtk.module new file mode 100644 index 000000000..c58d06aa4 --- /dev/null +++ b/Web/WebGLExporter/vtk.module @@ -0,0 +1,23 @@ +NAME + VTK::WebGLExporter +LIBRARY_NAME + vtkWebGLExporter +GROUPS + Web +SPDX_LICENSE_IDENTIFIER + BSD-3-Clause +SPDX_COPYRIGHT_TEXT + Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen +DEPENDS + VTK::CommonCore + VTK::IOExport +PRIVATE_DEPENDS + VTK::CommonDataModel + VTK::CommonMath + VTK::FiltersCore + VTK::FiltersGeometry + VTK::IOCore + VTK::InteractionWidgets + VTK::RenderingAnnotation + VTK::RenderingCore + VTK::vtksys diff --git a/Web/WebGLExporter/vtkPVWebGLExporter.cxx b/Web/WebGLExporter/vtkPVWebGLExporter.cxx new file mode 100644 index 000000000..3b8b9d82b --- /dev/null +++ b/Web/WebGLExporter/vtkPVWebGLExporter.cxx @@ -0,0 +1,125 @@ +// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen +// SPDX-License-Identifier: BSD-3-Clause +#include "vtkPVWebGLExporter.h" + +#include "vtkBase64Utilities.h" +#include "vtkCamera.h" +#include "vtkExporter.h" +#include "vtkNew.h" +#include "vtkObjectFactory.h" +#include "vtkRenderWindow.h" +#include "vtkRenderer.h" +#include "vtkRendererCollection.h" +#include "vtkWebGLExporter.h" +#include "vtkWebGLObject.h" + +#include +#include +#include +#include +#include + +VTK_ABI_NAMESPACE_BEGIN +vtkStandardNewMacro(vtkPVWebGLExporter); +//------------------------------------------------------------------------------ +vtkPVWebGLExporter::vtkPVWebGLExporter() +{ + this->FileName = nullptr; +} + +//------------------------------------------------------------------------------ +vtkPVWebGLExporter::~vtkPVWebGLExporter() +{ + this->SetFileName(nullptr); +} + +//------------------------------------------------------------------------------ +void vtkPVWebGLExporter::WriteData() +{ + // make sure the user specified a FileName or FilePointer + if (this->FileName == nullptr) + { + vtkErrorMacro(<< "Please specify FileName to use"); + return; + } + + vtkNew exporter; + exporter->SetMaxAllowedSize(65000); + + // We use the camera focal point to be the center of rotation + double centerOfRotation[3]; + vtkRenderer* ren = this->RenderWindow->GetRenderers()->GetFirstRenderer(); + vtkCamera* cam = ren->GetActiveCamera(); + cam->GetFocalPoint(centerOfRotation); + exporter->SetCenterOfRotation(static_cast(centerOfRotation[0]), + static_cast(centerOfRotation[1]), static_cast(centerOfRotation[2])); + + exporter->parseScene(this->RenderWindow->GetRenderers(), "1", VTK_PARSEALL); + + // Write meta-data file + std::string baseFileName = this->FileName; + baseFileName.erase(baseFileName.size() - 6, 6); + std::string metadatFile = this->FileName; + FILE* fp = vtksys::SystemTools::Fopen(metadatFile, "w"); + if (!fp) + { + vtkErrorMacro(<< "unable to open JSON MetaData file " << metadatFile); + return; + } + fputs(exporter->GenerateMetadata(), fp); + fclose(fp); + + // Write binary objects + vtkNew base64; + int nbObjects = exporter->GetNumberOfObjects(); + for (int idx = 0; idx < nbObjects; ++idx) + { + vtkWebGLObject* obj = exporter->GetWebGLObject(idx); + if (obj->isVisible()) + { + int nbParts = obj->GetNumberOfParts(); + for (int part = 0; part < nbParts; ++part) + { + // Manage binary content + std::stringstream filePath; + filePath << baseFileName << "_" << obj->GetMD5() << "_" << part; + vtksys::ofstream binaryFile; + binaryFile.open(filePath.str().c_str(), std::ios_base::out | std::ios_base::binary); + binaryFile.write((const char*)obj->GetBinaryData(part), obj->GetBinarySize(part)); + binaryFile.close(); + + // Manage Base64 + std::stringstream filePathBase64; + filePathBase64 << baseFileName << "_" << obj->GetMD5() << "_" << part << ".base64"; + vtksys::ofstream base64File; + unsigned char* output = new unsigned char[obj->GetBinarySize(part) * 2]; + int size = + base64->Encode(obj->GetBinaryData(part), obj->GetBinarySize(part), output, false); + base64File.open(filePathBase64.str().c_str(), std::ios_base::out); + base64File.write((const char*)output, size); + base64File.close(); + delete[] output; + } + } + } + + // Write HTML file + std::string htmlFile = baseFileName; + htmlFile += ".html"; + exporter->exportStaticScene(this->RenderWindow->GetRenderers(), 300, 300, htmlFile); +} +//------------------------------------------------------------------------------ +void vtkPVWebGLExporter::PrintSelf(ostream& os, vtkIndent indent) +{ + this->Superclass::PrintSelf(os, indent); + + if (this->FileName) + { + os << indent << "FileName: " << this->FileName << "\n"; + } + else + { + os << indent << "FileName: (null)\n"; + } +} +VTK_ABI_NAMESPACE_END diff --git a/Web/WebGLExporter/vtkPVWebGLExporter.h b/Web/WebGLExporter/vtkPVWebGLExporter.h new file mode 100644 index 000000000..4e74edeac --- /dev/null +++ b/Web/WebGLExporter/vtkPVWebGLExporter.h @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen +// SPDX-License-Identifier: BSD-3-Clause +#ifndef vtkPVWebGLExporter_h +#define vtkPVWebGLExporter_h + +#include "vtkExporter.h" +#include "vtkWebGLExporterModule.h" // needed for export macro + +VTK_ABI_NAMESPACE_BEGIN +class VTKWEBGLEXPORTER_EXPORT vtkPVWebGLExporter : public vtkExporter +{ +public: + static vtkPVWebGLExporter* New(); + vtkTypeMacro(vtkPVWebGLExporter, vtkExporter); + void PrintSelf(ostream& os, vtkIndent indent) override; + + // Description: + // Specify the name of the VRML file to write. + vtkSetFilePathMacro(FileName); + vtkGetFilePathMacro(FileName); + +protected: + vtkPVWebGLExporter(); + ~vtkPVWebGLExporter() override; + + void WriteData() override; + + char* FileName; + +private: + vtkPVWebGLExporter(const vtkPVWebGLExporter&) = delete; + void operator=(const vtkPVWebGLExporter&) = delete; +}; + +VTK_ABI_NAMESPACE_END +#endif diff --git a/Web/WebGLExporter/vtkWebGLDataSet.cxx b/Web/WebGLExporter/vtkWebGLDataSet.cxx new file mode 100644 index 000000000..ab89bc236 --- /dev/null +++ b/Web/WebGLExporter/vtkWebGLDataSet.cxx @@ -0,0 +1,235 @@ +// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen +// SPDX-License-Identifier: BSD-3-Clause + +#include "vtkWebGLDataSet.h" + +#include "vtkObjectFactory.h" +#include "vtkWebGLExporter.h" + +VTK_ABI_NAMESPACE_BEGIN +vtkStandardNewMacro(vtkWebGLDataSet); + +std::string vtkWebGLDataSet::GetMD5() +{ + return this->MD5; +} + +vtkWebGLDataSet::vtkWebGLDataSet() +{ + this->NumberOfVertices = 0; + this->NumberOfPoints = 0; + this->NumberOfIndexes = 0; + this->vertices = nullptr; + this->normals = nullptr; + this->indexes = nullptr; + this->points = nullptr; + this->tcoords = nullptr; + this->colors = nullptr; + this->binary = nullptr; + this->binarySize = 0; + this->hasChanged = false; +} + +vtkWebGLDataSet::~vtkWebGLDataSet() +{ + delete[] this->vertices; + delete[] this->normals; + delete[] this->indexes; + delete[] this->points; + delete[] this->tcoords; + delete[] this->colors; + delete[] this->binary; +} + +void vtkWebGLDataSet::SetVertices(float* v, int size) +{ + delete[] this->vertices; + this->vertices = v; + this->NumberOfVertices = size; + this->webGLType = wTRIANGLES; + this->hasChanged = true; +} + +void vtkWebGLDataSet::SetIndexes(short* i, int size) +{ + delete[] this->indexes; + this->indexes = i; + this->NumberOfIndexes = size; + this->hasChanged = true; +} + +void vtkWebGLDataSet::SetNormals(float* n) +{ + delete[] this->normals; + this->normals = n; + this->hasChanged = true; +} + +void vtkWebGLDataSet::SetColors(unsigned char* c) +{ + delete[] this->colors; + this->colors = c; + this->hasChanged = true; +} + +void vtkWebGLDataSet::SetPoints(float* p, int size) +{ + delete[] this->points; + this->points = p; + this->NumberOfPoints = size; + this->webGLType = wLINES; + this->hasChanged = true; +} + +void vtkWebGLDataSet::SetTCoords(float* t) +{ + delete[] this->tcoords; + this->tcoords = t; + this->hasChanged = true; +} + +unsigned char* vtkWebGLDataSet::GetBinaryData() +{ + this->hasChanged = false; + return this->binary; +} + +int vtkWebGLDataSet::GetBinarySize() +{ + return this->binarySize; +} + +void vtkWebGLDataSet::SetMatrix(float* m) +{ + this->Matrix = m; + this->hasChanged = true; +} + +void vtkWebGLDataSet::GenerateBinaryData() +{ + if (this->NumberOfIndexes == 0 && this->webGLType != wPOINTS) + { + return; + } + int size = 0, pos = 0, total = 0; + delete[] this->binary; + this->binarySize = 0; + + if (this->webGLType == wLINES) + { + pos = sizeof(pos); + size = this->NumberOfPoints * sizeof(this->points[0]); + + // Calculate the size used by each data + total = sizeof(pos) + 1 + sizeof(this->NumberOfPoints) + + size * 3 // Size, Type, NumberOfPoints, Points + + sizeof(this->colors[0]) * this->NumberOfPoints * 4 + + sizeof(this->NumberOfIndexes) // Color, NumberOfIndex + + this->NumberOfIndexes * sizeof(this->indexes[0]) + + sizeof(this->Matrix[0]) * 16; // Index, Matrix + this->binary = new unsigned char[total]; + memset(this->binary, 0, total); + + this->binary[pos++] = 'L'; + memcpy(&this->binary[pos], &this->NumberOfPoints, sizeof(this->NumberOfPoints)); + pos += sizeof(this->NumberOfPoints); // Points + memcpy(&this->binary[pos], this->points, size * 3); + pos += size * 3; + memcpy(&this->binary[pos], this->colors, sizeof(this->colors[0]) * this->NumberOfPoints * 4); + pos += sizeof(this->colors[0]) * this->NumberOfPoints * 4; + memcpy(&this->binary[pos], &this->NumberOfIndexes, sizeof(this->NumberOfIndexes)); + pos += sizeof(this->NumberOfIndexes); + memcpy(&this->binary[pos], this->indexes, this->NumberOfIndexes * sizeof(this->indexes[0])); + pos += this->NumberOfIndexes * sizeof(this->indexes[0]); + memcpy(&this->binary[pos], this->Matrix, sizeof(this->Matrix[0]) * 16); + pos += sizeof(this->Matrix[0]) * 16; // Matrix + + memcpy(&this->binary[0], &pos, sizeof(pos)); + this->binarySize = total; + } + else if (this->webGLType == wTRIANGLES) + { + pos = sizeof(pos); + size = sizeof(this->vertices[0]) * this->NumberOfVertices; + + // Calculate the size used by each data + total = sizeof(pos) + 1 + sizeof(this->NumberOfVertices) + + size * (3 + 3) // Size, Type, VertCount, Vert, Normal + + sizeof(this->colors[0]) * this->NumberOfVertices * 4 + + sizeof(this->NumberOfIndexes) // Color, IndicCount + + this->NumberOfIndexes * sizeof(this->indexes[0]) + + sizeof(this->Matrix[0]) * 16; // Index, Matrix + if (this->tcoords) + total += size * 2; // TCoord + this->binary = new unsigned char[total]; + memset(this->binary, 0, total); + + this->binary[pos++] = 'M'; + memcpy(&this->binary[pos], &this->NumberOfVertices, sizeof(this->NumberOfVertices)); + pos += sizeof(this->NumberOfVertices); // VertCount + memcpy(&this->binary[pos], this->vertices, size * 3); + pos += size * 3; // Vertices + memcpy(&this->binary[pos], this->normals, size * 3); + pos += size * 3; // Normals + memcpy(&this->binary[pos], this->colors, sizeof(this->colors[0]) * this->NumberOfVertices * 4); + pos += sizeof(this->colors[0]) * this->NumberOfVertices * 4; // Colors + memcpy(&this->binary[pos], &this->NumberOfIndexes, sizeof(this->NumberOfIndexes)); + pos += sizeof(this->NumberOfIndexes); // IndCount + memcpy(&this->binary[pos], this->indexes, this->NumberOfIndexes * sizeof(this->indexes[0])); + pos += this->NumberOfIndexes * sizeof(this->indexes[0]); + memcpy(&this->binary[pos], this->Matrix, sizeof(this->Matrix[0]) * 16); + pos += sizeof(this->Matrix[0]) * 16; // Matrix + if (this->tcoords) // TCoord + { + memcpy(&this->binary[pos], this->tcoords, size * 2); + pos += size * 2; + } + + memcpy(&this->binary[0], &pos, sizeof(pos)); + this->binarySize = total; + } + else if (this->webGLType == wPOINTS) + { + pos = sizeof(pos); + size = this->NumberOfPoints * sizeof(this->points[0]); + + // Calculate the size used by each data + total = sizeof(pos) + 1 + sizeof(this->NumberOfPoints) + + size * 3 // Size, Type, NumberOfPoints, Points + + sizeof(this->colors[0]) * this->NumberOfPoints * 4 + + sizeof(this->Matrix[0]) * 16; // Color, Matrix + this->binary = new unsigned char[total]; + memset(this->binary, 0, total); + + this->binary[pos++] = 'P'; + memcpy(&this->binary[pos], &this->NumberOfPoints, sizeof(this->NumberOfPoints)); + pos += sizeof(this->NumberOfPoints); // Points + memcpy(&this->binary[pos], this->points, size * 3); + pos += size * 3; + memcpy(&this->binary[pos], this->colors, sizeof(this->colors[0]) * this->NumberOfPoints * 4); + pos += sizeof(this->colors[0]) * this->NumberOfPoints * 4; + memcpy(&this->binary[pos], this->Matrix, sizeof(this->Matrix[0]) * 16); + pos += sizeof(this->Matrix[0]) * 16; // Matrix + + memcpy(&this->binary[0], &pos, sizeof(pos)); + this->binarySize = total; + } + vtkWebGLExporter::ComputeMD5((const unsigned char*)this->binary, this->binarySize, this->MD5); + this->hasChanged = true; +} + +void vtkWebGLDataSet::SetType(WebGLObjectTypes t) +{ + this->webGLType = t; +} + +bool vtkWebGLDataSet::HasChanged() +{ + return this->hasChanged; +} + +void vtkWebGLDataSet::PrintSelf(ostream& os, vtkIndent indent) +{ + this->Superclass::PrintSelf(os, indent); +} +VTK_ABI_NAMESPACE_END diff --git a/Web/WebGLExporter/vtkWebGLDataSet.h b/Web/WebGLExporter/vtkWebGLDataSet.h new file mode 100644 index 000000000..e86bc6af7 --- /dev/null +++ b/Web/WebGLExporter/vtkWebGLDataSet.h @@ -0,0 +1,68 @@ +// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen +// SPDX-License-Identifier: BSD-3-Clause +/** + * @class vtkWebGLDataSet + * @brief vtkWebGLDataSet represent vertices, lines, polygons, and triangles. + */ + +#ifndef vtkWebGLDataSet_h +#define vtkWebGLDataSet_h + +#include "vtkObject.h" +#include "vtkWebGLExporterModule.h" // needed for export macro + +#include "vtkWebGLObject.h" // Needed for the enum +#include // needed for md5 + +VTK_ABI_NAMESPACE_BEGIN +class VTKWEBGLEXPORTER_EXPORT vtkWebGLDataSet : public vtkObject +{ +public: + static vtkWebGLDataSet* New(); + vtkTypeMacro(vtkWebGLDataSet, vtkObject); + void PrintSelf(ostream& os, vtkIndent indent) override; + + void SetVertices(float* v, int size); + void SetIndexes(short* i, int size); + void SetNormals(float* n); + void SetColors(unsigned char* c); + void SetPoints(float* p, int size); + void SetTCoords(float* t); + void SetMatrix(float* m); + void SetType(WebGLObjectTypes t); + + unsigned char* GetBinaryData(); + int GetBinarySize(); + void GenerateBinaryData(); + bool HasChanged(); + + std::string GetMD5(); + +protected: + vtkWebGLDataSet(); + ~vtkWebGLDataSet() override; + + int NumberOfVertices; + int NumberOfPoints; + int NumberOfIndexes; + WebGLObjectTypes webGLType; + + float* Matrix; + float* vertices; + float* normals; + short* indexes; + float* points; + float* tcoords; + unsigned char* colors; + unsigned char* binary; // Data in binary + int binarySize; // Size of the data in binary + bool hasChanged; + std::string MD5; + +private: + vtkWebGLDataSet(const vtkWebGLDataSet&) = delete; + void operator=(const vtkWebGLDataSet&) = delete; +}; + +VTK_ABI_NAMESPACE_END +#endif diff --git a/Web/WebGLExporter/vtkWebGLExporter.cxx b/Web/WebGLExporter/vtkWebGLExporter.cxx new file mode 100644 index 000000000..f130c8853 --- /dev/null +++ b/Web/WebGLExporter/vtkWebGLExporter.cxx @@ -0,0 +1,788 @@ +// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen +// SPDX-License-Identifier: BSD-3-Clause + +#include "vtkWebGLExporter.h" + +#include "vtkAbstractMapper.h" +#include "vtkActor2D.h" +#include "vtkActorCollection.h" +#include "vtkBase64Utilities.h" +#include "vtkCamera.h" +#include "vtkCellArray.h" +#include "vtkCellData.h" +#include "vtkCompositeDataGeometryFilter.h" +#include "vtkCompositeDataSet.h" +#include "vtkDataSet.h" +#include "vtkDataSetAttributes.h" +#include "vtkDiscretizableColorTransferFunction.h" +#include "vtkFollower.h" +#include "vtkGenericCell.h" +#include "vtkMapper.h" +#include "vtkMapper2D.h" +#include "vtkMatrix4x4.h" +#include "vtkObjectFactory.h" +#include "vtkPointData.h" +#include "vtkPolyDataMapper2D.h" +#include "vtkPolyDataNormals.h" +#include "vtkProperty.h" +#include "vtkProperty2D.h" +#include "vtkRenderWindow.h" +#include "vtkRenderer.h" +#include "vtkRendererCollection.h" +#include "vtkScalarBarActor.h" +#include "vtkScalarBarRepresentation.h" +#include "vtkSmartPointer.h" +#include "vtkTriangleFilter.h" +#include "vtkViewport.h" +#include "vtkWidgetRepresentation.h" + +#include "vtkWebGLObject.h" +#include "vtkWebGLPolyData.h" +#include "vtkWebGLWidget.h" + +#include +#include +#include +#include +#include +#include + +#include "glMatrix.h" +#include "webglRenderer.h" + +#include "vtksys/FStream.hxx" +#include "vtksys/MD5.h" +#include "vtksys/SystemTools.hxx" + +//***************************************************************************** +class vtkWebGLExporter::vtkInternal +{ +public: + std::string LastMetaData; + std::map ActorTimestamp; + std::map OldActorTimestamp; + std::vector Objects; + std::vector tempObj; +}; +//***************************************************************************** + +vtkStandardNewMacro(vtkWebGLExporter); + +vtkWebGLExporter::vtkWebGLExporter() +{ + this->meshObjMaxSize = 65532 / 3; + this->lineObjMaxSize = 65534 / 2; + this->Internal = new vtkInternal(); + this->TriangleFilter = nullptr; + this->GradientBackground = false; + this->SetCenterOfRotation(0.0, 0.0, 0.0); + this->renderersMetaData = ""; + this->SceneSize[0] = 0; + this->SceneSize[1] = 0; + this->SceneSize[2] = 0; + this->hasWidget = false; +} + +vtkWebGLExporter::~vtkWebGLExporter() +{ + while (!this->Internal->Objects.empty()) + { + vtkWebGLObject* obj = this->Internal->Objects.back(); + obj->Delete(); + this->Internal->Objects.pop_back(); + } + delete this->Internal; + if (this->TriangleFilter) + { + this->TriangleFilter->Delete(); + } +} + +void vtkWebGLExporter::SetMaxAllowedSize(int mesh, int lines) +{ + this->meshObjMaxSize = mesh; + this->lineObjMaxSize = lines; + if (this->meshObjMaxSize * 3 > 65532) + this->meshObjMaxSize = 65532 / 3; + if (this->lineObjMaxSize * 2 > 65534) + this->lineObjMaxSize = 65534 / 2; + if (this->meshObjMaxSize < 10) + this->meshObjMaxSize = 10; + if (this->lineObjMaxSize < 10) + this->lineObjMaxSize = 10; + for (size_t i = 0; i < this->Internal->Objects.size(); i++) + this->Internal->Objects[i]->GenerateBinaryData(); +} + +void vtkWebGLExporter::SetMaxAllowedSize(int size) +{ + this->SetMaxAllowedSize(size, size); +} + +void vtkWebGLExporter::SetCenterOfRotation(float a1, float a2, float a3) +{ + this->CenterOfRotation[0] = a1; + this->CenterOfRotation[1] = a2; + this->CenterOfRotation[2] = a3; +} + +void vtkWebGLExporter::parseRenderer( + vtkRenderer* renderer, const char* vtkNotUsed(viewId), bool onlyWidget, void* vtkNotUsed(mapTime)) +{ + vtkPropCollection* propCollection = renderer->GetViewProps(); + for (int i = 0; i < propCollection->GetNumberOfItems(); i++) + { + vtkProp* prop = (vtkProp*)propCollection->GetItemAsObject(i); + vtkWidgetRepresentation* trt = vtkWidgetRepresentation::SafeDownCast(prop); + if (trt != nullptr) + this->hasWidget = true; + if ((!onlyWidget || trt != nullptr) && prop->GetVisibility()) + { + vtkPropCollection* allactors = vtkPropCollection::New(); + prop->GetActors(allactors); + for (int j = 0; j < allactors->GetNumberOfItems(); j++) + { + vtkActor* actor = vtkActor::SafeDownCast(allactors->GetItemAsObject(j)); + vtkActor* key = actor; + vtkMTimeType previousValue = this->Internal->OldActorTimestamp[key]; + this->parseActor( + actor, previousValue, (size_t)renderer, renderer->GetLayer(), trt != nullptr); + } + allactors->Delete(); + } + if (!onlyWidget && prop->GetVisibility()) + { + vtkPropCollection* all2dactors = vtkPropCollection::New(); + prop->GetActors2D(all2dactors); + for (int k = 0; k < all2dactors->GetNumberOfItems(); k++) + { + vtkActor2D* actor = vtkActor2D::SafeDownCast(all2dactors->GetItemAsObject(k)); + vtkActor2D* key = actor; + vtkMTimeType previousValue = this->Internal->OldActorTimestamp[key]; + this->parseActor2D( + actor, previousValue, (size_t)renderer, renderer->GetLayer(), trt != nullptr); + } + all2dactors->Delete(); + } + } +} + +void vtkWebGLExporter::parseActor2D( + vtkActor2D* actor, vtkMTimeType actorTime, size_t renderId, int layer, bool isWidget) +{ + vtkActor2D* key = actor; + vtkScalarBarActor* scalarbar = vtkScalarBarActor::SafeDownCast(actor); + + vtkMTimeType dataMTime = + actor->GetMTime() + actor->GetRedrawMTime() + actor->GetProperty()->GetMTime(); + dataMTime += (vtkMTimeType)actor->GetMapper(); + if (scalarbar) + dataMTime += scalarbar->GetLookupTable()->GetMTime(); + if (dataMTime != actorTime && actor->GetVisibility()) + { + this->Internal->ActorTimestamp[key] = dataMTime; + + if (actor->GetMapper()) + { + if (vtkPolyDataMapper2D::SafeDownCast(actor->GetMapper())) + { + } + } + else + { + if (scalarbar) + { + vtkWebGLWidget* obj = vtkWebGLWidget::New(); + obj->GetDataFromColorMap(actor); + + std::stringstream ss; + ss << (size_t)actor; + obj->SetId(ss.str()); + obj->SetRendererId(static_cast(renderId)); + this->Internal->Objects.push_back(obj); + obj->SetLayer(layer); + obj->SetVisibility(actor->GetVisibility() != 0); + obj->SetIsWidget(isWidget); + obj->SetInteractAtServer(false); + obj->GenerateBinaryData(); + } + } + } + else + { + this->Internal->ActorTimestamp[key] = dataMTime; + std::stringstream ss; + ss << (vtkMTimeType)actor; + for (size_t i = 0; i < this->Internal->tempObj.size(); i++) + { + if (this->Internal->tempObj[i]->GetId() == ss.str()) + { + vtkWebGLObject* obj = this->Internal->tempObj[i]; + this->Internal->tempObj.erase(this->Internal->tempObj.begin() + i); + obj->SetVisibility(actor->GetVisibility() != 0); + this->Internal->Objects.push_back(obj); + } + } + } +} + +void vtkWebGLExporter::parseActor( + vtkActor* actor, vtkMTimeType actorTime, size_t rendererId, int layer, bool isWidget) +{ + vtkMapper* mapper = actor->GetMapper(); + if (mapper) + { + vtkMTimeType dataMTime; + vtkTriangleFilter* polydata = this->GetPolyData(mapper, dataMTime); + vtkActor* key = actor; + dataMTime = actor->GetMTime() + mapper->GetLookupTable()->GetMTime(); + dataMTime += actor->GetProperty()->GetMTime() + mapper->GetMTime() + actor->GetRedrawMTime(); + dataMTime += + polydata->GetOutput()->GetNumberOfLines() + polydata->GetOutput()->GetNumberOfPolys(); + dataMTime += + actor->GetProperty()->GetRepresentation() + mapper->GetScalarMode() + actor->GetVisibility(); + dataMTime += polydata->GetInput()->GetMTime(); + if (vtkFollower::SafeDownCast(actor)) + dataMTime += vtkFollower::SafeDownCast(actor)->GetCamera()->GetMTime(); + if (dataMTime != actorTime && actor->GetVisibility()) + { + double bb[6]; + actor->GetBounds(bb); + double m1 = std::max(bb[1] - bb[0], bb[3] - bb[2]); + m1 = std::max(m1, bb[5] - bb[4]); + double m2 = std::max(this->SceneSize[0], this->SceneSize[1]); + m2 = std::max(m2, this->SceneSize[2]); + if (m1 > m2) + { + this->SceneSize[0] = bb[1] - bb[0]; + this->SceneSize[1] = bb[3] - bb[2]; + this->SceneSize[2] = bb[5] - bb[4]; + } + + this->Internal->ActorTimestamp[key] = dataMTime; + vtkWebGLObject* obj = nullptr; + std::stringstream ss; + ss << (size_t)actor; + for (size_t i = 0; i < this->Internal->tempObj.size(); i++) + { + if (this->Internal->tempObj[i]->GetId() == ss.str()) + { + obj = this->Internal->tempObj[i]; + this->Internal->tempObj.erase(this->Internal->tempObj.begin() + i); + } + } + if (obj == nullptr) + obj = vtkWebGLPolyData::New(); + + if (polydata->GetOutput()->GetNumberOfPolys() != 0) + { + if (actor->GetProperty()->GetRepresentation() == VTK_WIREFRAME) + { + ((vtkWebGLPolyData*)obj) + ->GetLinesFromPolygon(mapper, actor, this->lineObjMaxSize, nullptr); + } + else + { + + if (actor->GetProperty()->GetEdgeVisibility()) + { + vtkWebGLPolyData* newobj = vtkWebGLPolyData::New(); + double ccc[3]; + actor->GetProperty()->GetEdgeColor(&ccc[0]); + newobj->GetLinesFromPolygon(mapper, actor, this->lineObjMaxSize, ccc); + newobj->SetId(ss.str() + "1"); + newobj->SetRendererId(static_cast(rendererId)); + this->Internal->Objects.push_back(newobj); + newobj->SetLayer(layer); + newobj->SetTransformationMatrix(actor->GetMatrix()); + newobj->SetVisibility(actor->GetVisibility() != 0); + newobj->SetHasTransparency(actor->HasTranslucentPolygonalGeometry() != 0); + newobj->SetIsWidget(isWidget); + newobj->SetInteractAtServer(isWidget); + newobj->GenerateBinaryData(); + } + + switch (mapper->GetScalarMode()) + { + case VTK_SCALAR_MODE_USE_POINT_FIELD_DATA: + ((vtkWebGLPolyData*)obj) + ->GetPolygonsFromPointData(polydata, actor, this->meshObjMaxSize); + break; + case VTK_SCALAR_MODE_USE_CELL_FIELD_DATA: + ((vtkWebGLPolyData*)obj) + ->GetPolygonsFromCellData(polydata, actor, this->meshObjMaxSize); + break; + default: + ((vtkWebGLPolyData*)obj) + ->GetPolygonsFromPointData(polydata, actor, this->meshObjMaxSize); + break; + } + } + obj->SetId(ss.str()); + obj->SetRendererId(static_cast(rendererId)); + this->Internal->Objects.push_back(obj); + obj->SetLayer(layer); + obj->SetTransformationMatrix(actor->GetMatrix()); + obj->SetVisibility(actor->GetVisibility() != 0); + obj->SetHasTransparency(actor->HasTranslucentPolygonalGeometry() != 0); + obj->SetIsWidget(isWidget); + obj->SetInteractAtServer(isWidget); + obj->GenerateBinaryData(); + } + else if (polydata->GetOutput()->GetNumberOfLines() != 0) + { + ((vtkWebGLPolyData*)obj)->GetLines(polydata, actor, this->lineObjMaxSize); + obj->SetId(ss.str()); + obj->SetRendererId(static_cast(rendererId)); + this->Internal->Objects.push_back(obj); + obj->SetLayer(layer); + obj->SetTransformationMatrix(actor->GetMatrix()); + obj->SetVisibility(actor->GetVisibility() != 0); + obj->SetHasTransparency(actor->HasTranslucentPolygonalGeometry() != 0); + obj->SetIsWidget(isWidget); + obj->SetInteractAtServer(isWidget); + obj->GenerateBinaryData(); + } + else if (polydata->GetOutput()->GetNumberOfPoints() != 0) + { + ((vtkWebGLPolyData*)obj)->GetPoints(polydata, actor, 65534); // Wendel + obj->SetId(ss.str()); + obj->SetRendererId(static_cast(rendererId)); + this->Internal->Objects.push_back(obj); + obj->SetLayer(layer); + obj->SetTransformationMatrix(actor->GetMatrix()); + obj->SetVisibility(actor->GetVisibility() != 0); + obj->SetHasTransparency(actor->HasTranslucentPolygonalGeometry() != 0); + obj->SetIsWidget(false); + obj->SetInteractAtServer(false); + obj->GenerateBinaryData(); + } + + if (polydata->GetOutput()->GetNumberOfPolys() != 0 && + polydata->GetOutput()->GetNumberOfLines() != 0) + { + obj = vtkWebGLPolyData::New(); + ((vtkWebGLPolyData*)obj)->GetLines(polydata, actor, this->lineObjMaxSize); + ss << "1"; + obj->SetId(ss.str()); + obj->SetRendererId(static_cast(rendererId)); + this->Internal->Objects.push_back(obj); + obj->SetLayer(layer); + obj->SetTransformationMatrix(actor->GetMatrix()); + obj->SetVisibility(actor->GetVisibility() != 0); + obj->SetHasTransparency(actor->HasTranslucentPolygonalGeometry() != 0); + obj->SetIsWidget(isWidget); + obj->SetInteractAtServer(isWidget); + obj->GenerateBinaryData(); + } + + if (polydata->GetOutput()->GetNumberOfLines() == 0 && + polydata->GetOutput()->GetNumberOfPolys() == 0 && + polydata->GetOutput()->GetNumberOfPoints() == 0) + { + obj->Delete(); + } + } + else + { + this->Internal->ActorTimestamp[key] = actorTime; + std::stringstream ss; + ss << (size_t)actor; + for (size_t i = 0; i < this->Internal->tempObj.size(); i++) + { + if (this->Internal->tempObj[i]->GetId() == ss.str()) + { + vtkWebGLObject* obj = this->Internal->tempObj[i]; + this->Internal->tempObj.erase(this->Internal->tempObj.begin() + i); + obj->SetVisibility(actor->GetVisibility() != 0); + this->Internal->Objects.push_back(obj); + } + } + } + } +} + +void vtkWebGLExporter::parseScene( + vtkRendererCollection* renderers, const char* viewId, int parseType) +{ + if (!renderers) + return; + + bool onlyWidget = parseType == VTK_ONLYWIDGET; + bool cameraOnly = onlyWidget && !this->hasWidget; + + this->SceneId = viewId ? viewId : ""; + if (cameraOnly) + { + this->generateRendererData(renderers, viewId); + return; + } + + if (onlyWidget) + { + for (int i = static_cast(this->Internal->Objects.size()) - 1; i >= 0; i--) + { + vtkWebGLObject* obj = this->Internal->Objects[i]; + if (obj->InteractAtServer()) + { + this->Internal->tempObj.push_back(obj); + this->Internal->Objects.erase(this->Internal->Objects.begin() + i); + } + } + } + else + { + while (!this->Internal->Objects.empty()) + { + this->Internal->tempObj.push_back(this->Internal->Objects.back()); + this->Internal->Objects.pop_back(); + } + } + + this->Internal->OldActorTimestamp = this->Internal->ActorTimestamp; + if (!onlyWidget) + this->Internal->ActorTimestamp.clear(); + this->hasWidget = false; + for (int i = 0; i < renderers->GetNumberOfItems(); i++) + { + vtkRenderer* renderer = vtkRenderer::SafeDownCast(renderers->GetItemAsObject(i)); + if (renderer->GetDraw()) + this->parseRenderer(renderer, viewId, onlyWidget, nullptr); + } + while (!this->Internal->tempObj.empty()) + { + vtkWebGLObject* obj = this->Internal->tempObj.back(); + this->Internal->tempObj.pop_back(); + obj->Delete(); + } + + this->generateRendererData(renderers, viewId); +} + +bool sortLayer(vtkRenderer* i, vtkRenderer* j) +{ + return (i->GetLayer() < j->GetLayer()); +} + +void vtkWebGLExporter::generateRendererData( + vtkRendererCollection* renderers, const char* vtkNotUsed(viewId)) +{ + std::stringstream ss; + ss << "\"Renderers\": ["; + + std::vector orderedList; + orderedList.reserve(renderers->GetNumberOfItems()); + for (int i = 0; i < renderers->GetNumberOfItems(); i++) + orderedList.push_back(vtkRenderer::SafeDownCast(renderers->GetItemAsObject(i))); + std::sort(orderedList.begin(), orderedList.begin() + orderedList.size(), sortLayer); + + int* fullSize = nullptr; + for (size_t i = 0; i < orderedList.size(); i++) + { + vtkRenderer* renderer = orderedList[i]; + + if (i == 0) + { + fullSize = renderer->GetSize(); + } + + double cam[10]; + cam[0] = renderer->GetActiveCamera()->GetViewAngle(); + renderer->GetActiveCamera()->GetFocalPoint(&cam[1]); + renderer->GetActiveCamera()->GetViewUp(&cam[4]); + renderer->GetActiveCamera()->GetPosition(&cam[7]); + int *s, *o; + s = renderer->GetSize(); + o = renderer->GetOrigin(); + ss << "{\"layer\":" << renderer->GetLayer() << ","; // Render Layer + if (renderer->GetLayer() == 0) // Render Background + { + double back[3]; + renderer->GetBackground(back); + ss << "\"Background1\":[" << back[0] << "," << back[1] << "," << back[2] << "],"; + if (renderer->GetGradientBackground()) + { + renderer->GetBackground2(back); + ss << "\"Background2\":[" << back[0] << "," << back[1] << "," << back[2] << "],"; + } + } + ss << "\"LookAt\":["; // Render Camera + for (int j = 0; j < 9; j++) + ss << cam[j] << ","; + ss << cam[9] << "], "; + ss << "\"size\": [" << (float)(s[0] / (float)fullSize[0]) << "," + << (float)(s[1] / (float)fullSize[1]) << "],"; // Render Size + ss << "\"origin\": [" << (float)(o[0] / (float)fullSize[0]) << "," + << (float)(o[1] / (float)fullSize[1]) << "]"; // Render Position + ss << "}"; + if (static_cast(i + 1) != renderers->GetNumberOfItems()) + ss << ", "; + } + ss << "]"; + this->renderersMetaData = ss.str(); +} + +vtkTriangleFilter* vtkWebGLExporter::GetPolyData(vtkMapper* mapper, vtkMTimeType& dataMTime) +{ + vtkDataSet* dataset = nullptr; + vtkSmartPointer tempDS; + vtkDataObject* dObj = mapper->GetInputDataObject(0, 0); + vtkCompositeDataSet* cd = vtkCompositeDataSet::SafeDownCast(dObj); + if (cd) + { + dataMTime = cd->GetMTime(); + vtkCompositeDataGeometryFilter* gf = vtkCompositeDataGeometryFilter::New(); + gf->SetInputData(cd); + gf->Update(); + tempDS = gf->GetOutput(); + gf->Delete(); + dataset = tempDS; + } + else + { + dataset = mapper->GetInput(); + dataMTime = dataset->GetMTime(); + } + + // Converting to triangles. WebGL only support triangles. + if (this->TriangleFilter) + this->TriangleFilter->Delete(); + this->TriangleFilter = vtkTriangleFilter::New(); + this->TriangleFilter->SetInputData(dataset); + this->TriangleFilter->Update(); + return this->TriangleFilter; +} + +/* + Function: GenerateMetaData + Description: + - Generates the metadata of the scene in JSON format + Ex.: + { "id": ,"LookAt": ,"Background1": ,"Background2": + "Objects": [{"id": ,"md5": ,"parts": }, {"id": ,"md5": ,"parts": }] } +*/ +VTK_ABI_NAMESPACE_BEGIN +const char* vtkWebGLExporter::GenerateMetadata() +{ + double max = std::max(this->SceneSize[0], this->SceneSize[1]); + max = std::max(max, this->SceneSize[2]); + std::stringstream ss; + + ss << "{\"id\":" << this->SceneId << ","; + ss << "\"MaxSize\":" << max << ","; + ss << "\"Center\":["; + for (int i = 0; i < 2; i++) + ss << this->CenterOfRotation[i] << ", "; + ss << this->CenterOfRotation[2] << "],"; + + ss << this->renderersMetaData << ","; + + ss << " \"Objects\":["; + bool first = true; + for (size_t i = 0; i < this->Internal->Objects.size(); i++) + { + vtkWebGLObject* obj = this->Internal->Objects[i]; + if (obj->isVisible()) + { + if (first) + first = false; + else + ss << ", "; + ss << "{\"id\":" << obj->GetId() << ", \"md5\":\"" << obj->GetMD5() << "\"" + << ", \"parts\":" << obj->GetNumberOfParts() + << ", \"interactAtServer\":" << obj->InteractAtServer() + << ", \"transparency\":" << obj->HasTransparency() << ", \"layer\":" << obj->GetLayer() + << ", \"wireframe\":" << obj->isWireframeMode() << "}"; + } + } + ss << "]}"; + + this->Internal->LastMetaData = ss.str(); + return this->Internal->LastMetaData.c_str(); +} + +const char* vtkWebGLExporter::GenerateExportMetadata() +{ + double max = std::max(this->SceneSize[0], this->SceneSize[1]); + max = std::max(max, this->SceneSize[2]); + std::stringstream ss; + + ss << "{\"id\":" << this->SceneId << ","; + ss << "\"MaxSize\":" << max << ","; + ss << "\"Center\":["; + for (int i = 0; i < 2; i++) + ss << this->CenterOfRotation[i] << ", "; + ss << this->CenterOfRotation[2] << "],"; + + ss << this->renderersMetaData << ","; + + ss << " \"Objects\":["; + bool first = true; + for (size_t i = 0; i < this->Internal->Objects.size(); i++) + { + vtkWebGLObject* obj = this->Internal->Objects[i]; + if (obj->isVisible()) + { + for (int j = 0; j < obj->GetNumberOfParts(); j++) + { + if (first) + first = false; + else + ss << ", "; + ss << "{\"id\":" << obj->GetId() << ", \"md5\":\"" << obj->GetMD5() << "\"" + << ", \"parts\":" << 1 << ", \"interactAtServer\":" << obj->InteractAtServer() + << ", \"transparency\":" << obj->HasTransparency() << ", \"layer\":" << obj->GetLayer() + << ", \"wireframe\":" << obj->isWireframeMode() << "}"; + } + } + } + ss << "]}"; + + this->Internal->LastMetaData = ss.str(); + return this->Internal->LastMetaData.c_str(); +} + +vtkWebGLObject* vtkWebGLExporter::GetWebGLObject(int index) +{ + return this->Internal->Objects[index]; +} + +int vtkWebGLExporter::GetNumberOfObjects() +{ + return static_cast(this->Internal->Objects.size()); +} + +void vtkWebGLExporter::PrintSelf(ostream& os, vtkIndent indent) +{ + this->Superclass::PrintSelf(os, indent); +} + +const char* vtkWebGLExporter::GetId() +{ + return this->SceneId.c_str(); +} + +bool vtkWebGLExporter::hasChanged() +{ + for (size_t i = 0; i < this->Internal->Objects.size(); i++) + if (this->Internal->Objects[i]->HasChanged()) + return true; + return false; +} + +void vtkWebGLExporter::exportStaticScene( + vtkRendererCollection* renderers, int width, int height, std::string path) +{ + std::stringstream ss; + ss << width << "," << height; + std::string resultHTML = + ""; + resultHTML += "
\n"; + resultHTML += ""; + + vtksys::ofstream file; + file.open(path.c_str()); + file << resultHTML; + file.close(); +} + +//------------------------------------------------------------------------------ +void vtkWebGLExporter::ComputeMD5(const unsigned char* content, int size, std::string& hash) +{ + unsigned char digest[16]; + char md5Hash[33]; + md5Hash[32] = '\0'; + + vtksysMD5* md5 = vtksysMD5_New(); + vtksysMD5_Initialize(md5); + vtksysMD5_Append(md5, content, size); + vtksysMD5_Finalize(md5, digest); + vtksysMD5_DigestToHex(digest, md5Hash); + vtksysMD5_Delete(md5); + + hash = md5Hash; +} +VTK_ABI_NAMESPACE_END diff --git a/Web/WebGLExporter/vtkWebGLExporter.h b/Web/WebGLExporter/vtkWebGLExporter.h new file mode 100644 index 000000000..eeae073b1 --- /dev/null +++ b/Web/WebGLExporter/vtkWebGLExporter.h @@ -0,0 +1,101 @@ +// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen +// SPDX-License-Identifier: BSD-3-Clause +/** + * @class vtkWebGLExporter + * @brief vtkWebGLExporter export the data of the scene to be used in the WebGL. + */ + +#ifndef vtkWebGLExporter_h +#define vtkWebGLExporter_h + +#include "vtkObject.h" +#include "vtkWebGLExporterModule.h" // needed for export macro + +VTK_ABI_NAMESPACE_BEGIN +class vtkActor; +class vtkActor2D; +class vtkCellData; +class vtkMapper; +class vtkPointData; +class vtkPolyData; +class vtkRenderer; +class vtkRendererCollection; +class vtkTriangleFilter; +class vtkWebGLObject; +class vtkWebGLPolyData; + +VTK_ABI_NAMESPACE_END + +#include // needed for internal structure + +VTK_ABI_NAMESPACE_BEGIN +typedef enum +{ + VTK_ONLYCAMERA = 0, + VTK_ONLYWIDGET = 1, + VTK_PARSEALL = 2 +} VTKParseType; + +class VTKWEBGLEXPORTER_EXPORT vtkWebGLExporter : public vtkObject +{ +public: + static vtkWebGLExporter* New(); + vtkTypeMacro(vtkWebGLExporter, vtkObject); + void PrintSelf(ostream& os, vtkIndent indent) override; + + ///@{ + /** + * Get all the needed information from the vtkRenderer + */ + void parseScene(vtkRendererCollection* renderers, const char* viewId, int parseType); + // Generate and return the Metadata + void exportStaticScene(vtkRendererCollection* renderers, int width, int height, std::string path); + const char* GenerateMetadata(); + const char* GetId(); + vtkWebGLObject* GetWebGLObject(int index); + int GetNumberOfObjects(); + bool hasChanged(); + void SetCenterOfRotation(float a1, float a2, float a3); + void SetMaxAllowedSize(int mesh, int lines); + void SetMaxAllowedSize(int size); + ///@} + + static void ComputeMD5(const unsigned char* content, int size, std::string& hash); + +protected: + vtkWebGLExporter(); + ~vtkWebGLExporter() override; + + void parseRenderer(vtkRenderer* render, const char* viewId, bool onlyWidget, void* mapTime); + void generateRendererData(vtkRendererCollection* renderers, const char* viewId); + void parseActor( + vtkActor* actor, vtkMTimeType actorTime, size_t rendererId, int layer, bool isWidget); + void parseActor2D( + vtkActor2D* actor, vtkMTimeType actorTime, size_t renderId, int layer, bool isWidget); + const char* GenerateExportMetadata(); + + // Get the dataset from the mapper + vtkTriangleFilter* GetPolyData(vtkMapper* mapper, vtkMTimeType& dataMTime); + + vtkTriangleFilter* TriangleFilter; // Last Polygon Dataset Parse + double CameraLookAt[10]; // Camera Look At (fov, position[3], up[3], eye[3]) + bool GradientBackground; // If the scene use a gradient background + double Background1[3]; // Background color of the rendering screen (RGB) + double Background2[3]; // Second background color + double SceneSize[3]; // Size of the bounding box of the scene + std::string SceneId; // Id of the parsed scene + float CenterOfRotation[3]; // Center Of Rotation + int meshObjMaxSize, lineObjMaxSize; // Max size of object allowed (faces) + std::string renderersMetaData; + bool hasWidget; + +private: + vtkWebGLExporter(const vtkWebGLExporter&) = delete; + void operator=(const vtkWebGLExporter&) = delete; + + class vtkInternal; + vtkInternal* Internal; +}; + +VTK_ABI_NAMESPACE_END +#endif diff --git a/Web/WebGLExporter/vtkWebGLObject.cxx b/Web/WebGLExporter/vtkWebGLObject.cxx new file mode 100644 index 000000000..70891a4df --- /dev/null +++ b/Web/WebGLExporter/vtkWebGLObject.cxx @@ -0,0 +1,201 @@ +// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen +// SPDX-License-Identifier: BSD-3-Clause + +#include "vtkWebGLObject.h" + +#include "vtkMatrix4x4.h" +#include "vtkObjectFactory.h" +#include "vtkUnsignedCharArray.h" + +#include + +VTK_ABI_NAMESPACE_BEGIN +vtkStandardNewMacro(vtkWebGLObject); +VTK_ABI_NAMESPACE_END +#include +#include + +//------------------------------------------------------------------------------ +VTK_ABI_NAMESPACE_BEGIN +vtkWebGLObject::vtkWebGLObject() +{ + this->iswireframeMode = false; + this->hasChanged = false; + this->webGlType = wTRIANGLES; + this->hasTransparency = false; + this->iswidget = false; + this->interactAtServer = false; +} + +//------------------------------------------------------------------------------ +vtkWebGLObject::~vtkWebGLObject() = default; + +//------------------------------------------------------------------------------ +std::string vtkWebGLObject::GetId() +{ + return this->id; +} + +//------------------------------------------------------------------------------ +void vtkWebGLObject::SetId(const std::string& i) +{ + this->id = i; +} + +//------------------------------------------------------------------------------ +void vtkWebGLObject::SetType(WebGLObjectTypes t) +{ + this->webGlType = t; +} + +//------------------------------------------------------------------------------ +void vtkWebGLObject::SetTransformationMatrix(vtkMatrix4x4* m) +{ + for (int i = 0; i < 16; i++) + this->Matrix[i] = m->GetElement(i / 4, i % 4); +} + +//------------------------------------------------------------------------------ +std::string vtkWebGLObject::GetMD5() +{ + return this->MD5; +} + +//------------------------------------------------------------------------------ +void vtkWebGLObject::PrintSelf(ostream& os, vtkIndent indent) +{ + this->Superclass::PrintSelf(os, indent); +} + +//------------------------------------------------------------------------------ +bool vtkWebGLObject::HasChanged() +{ + return this->hasChanged; +} + +//------------------------------------------------------------------------------ +void vtkWebGLObject::SetWireframeMode(bool wireframe) +{ + this->iswireframeMode = wireframe; +} + +//------------------------------------------------------------------------------ +bool vtkWebGLObject::isWireframeMode() +{ + return this->iswireframeMode; +} + +//------------------------------------------------------------------------------ +void vtkWebGLObject::SetVisibility(bool vis) +{ + this->isvisible = vis; +} + +//------------------------------------------------------------------------------ +bool vtkWebGLObject::isVisible() +{ + return this->isvisible; +} + +//------------------------------------------------------------------------------ +void vtkWebGLObject::SetHasTransparency(bool t) +{ + this->hasTransparency = t; +} + +//------------------------------------------------------------------------------ +void vtkWebGLObject::SetIsWidget(bool w) +{ + this->iswidget = w; +} + +//------------------------------------------------------------------------------ +bool vtkWebGLObject::isWidget() +{ + return this->iswidget; +} + +//------------------------------------------------------------------------------ +bool vtkWebGLObject::HasTransparency() +{ + return this->hasTransparency; +} + +//------------------------------------------------------------------------------ +void vtkWebGLObject::SetRendererId(size_t i) +{ + this->rendererId = i; +} + +//------------------------------------------------------------------------------ +size_t vtkWebGLObject::GetRendererId() +{ + return this->rendererId; +} + +//------------------------------------------------------------------------------ +void vtkWebGLObject::SetLayer(int l) +{ + this->layer = l; +} + +//------------------------------------------------------------------------------ +int vtkWebGLObject::GetLayer() +{ + return this->layer; +} + +//------------------------------------------------------------------------------ +bool vtkWebGLObject::InteractAtServer() +{ + return this->interactAtServer; +} + +//------------------------------------------------------------------------------ +void vtkWebGLObject::SetInteractAtServer(bool i) +{ + this->interactAtServer = i; +} + +//------------------------------------------------------------------------------ +void vtkWebGLObject::GetBinaryData(int part, vtkUnsignedCharArray* buffer) +{ + if (!buffer) + { + vtkErrorMacro("Buffer must not be nullptr."); + return; + } + + const int binarySize = this->GetBinarySize(part); + const unsigned char* binaryData = this->GetBinaryData(part); + + buffer->SetNumberOfComponents(1); + buffer->SetNumberOfTuples(binarySize); + + if (binarySize) + { + std::copy(binaryData, binaryData + binarySize, buffer->GetPointer(0)); + } +} + +//------------------------------------------------------------------------------ +void vtkWebGLObject::GenerateBinaryData() +{ + this->hasChanged = false; +} +//------------------------------------------------------------------------------ +unsigned char* vtkWebGLObject::GetBinaryData(int vtkNotUsed(part)) +{ + return nullptr; +} +//------------------------------------------------------------------------------ +int vtkWebGLObject::GetBinarySize(int vtkNotUsed(part)) +{ + return 0; +} +//------------------------------------------------------------------------------ +int vtkWebGLObject::GetNumberOfParts() +{ + return 0; +} +VTK_ABI_NAMESPACE_END diff --git a/Web/WebGLExporter/vtkWebGLObject.h b/Web/WebGLExporter/vtkWebGLObject.h new file mode 100644 index 000000000..4bd67b903 --- /dev/null +++ b/Web/WebGLExporter/vtkWebGLObject.h @@ -0,0 +1,92 @@ +// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen +// SPDX-License-Identifier: BSD-3-Clause +/** + * @class vtkWebGLObject + * @brief vtkWebGLObject represent and manipulate an WebGL object and its data. + */ + +#ifndef vtkWebGLObject_h +#define vtkWebGLObject_h + +#include "vtkObject.h" +#include "vtkWebGLExporterModule.h" // needed for export macro + +#include // needed for ID and md5 storing + +VTK_ABI_NAMESPACE_BEGIN +class vtkMatrix4x4; +class vtkUnsignedCharArray; + +enum WebGLObjectTypes +{ + wPOINTS = 0, + wLINES = 1, + wTRIANGLES = 2 +}; + +class VTKWEBGLEXPORTER_EXPORT vtkWebGLObject : public vtkObject +{ +public: + static vtkWebGLObject* New(); + vtkTypeMacro(vtkWebGLObject, vtkObject); + void PrintSelf(ostream& os, vtkIndent indent) override; + + virtual void GenerateBinaryData(); + virtual unsigned char* GetBinaryData(int part); + virtual int GetBinarySize(int part); + virtual int GetNumberOfParts(); + + /** + * This is a wrapper friendly method for access the binary data. + * The binary data for the requested part will be copied into the + * given vtkUnsignedCharArray. + */ + void GetBinaryData(int part, vtkUnsignedCharArray* buffer); + + void SetLayer(int l); + void SetRendererId(size_t i); + void SetId(const std::string& i); + void SetWireframeMode(bool wireframe); + void SetVisibility(bool vis); + void SetTransformationMatrix(vtkMatrix4x4* m); + void SetIsWidget(bool w); + void SetHasTransparency(bool t); + void SetInteractAtServer(bool i); + void SetType(WebGLObjectTypes t); + bool isWireframeMode(); + bool isVisible(); + bool HasChanged(); + bool isWidget(); + bool HasTransparency(); + bool InteractAtServer(); + + std::string GetMD5(); + std::string GetId(); + + size_t GetRendererId(); + int GetLayer(); + +protected: + vtkWebGLObject(); + ~vtkWebGLObject() override; + + float Matrix[16]; + size_t rendererId; + int layer; // Renderer Layer + std::string id; // Id of the object + std::string MD5; + bool hasChanged; + bool iswireframeMode; + bool isvisible; + WebGLObjectTypes webGlType; + bool hasTransparency; + bool iswidget; + bool interactAtServer; + +private: + vtkWebGLObject(const vtkWebGLObject&) = delete; + void operator=(const vtkWebGLObject&) = delete; +}; + +VTK_ABI_NAMESPACE_END +#endif diff --git a/Web/WebGLExporter/vtkWebGLPolyData.cxx b/Web/WebGLExporter/vtkWebGLPolyData.cxx new file mode 100644 index 000000000..7dae8a23c --- /dev/null +++ b/Web/WebGLExporter/vtkWebGLPolyData.cxx @@ -0,0 +1,783 @@ +// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen +// SPDX-License-Identifier: BSD-3-Clause + +#include "vtkWebGLPolyData.h" + +#include "vtkActor.h" +#include "vtkCell.h" +#include "vtkCellArray.h" +#include "vtkCellData.h" +#include "vtkCompositeDataGeometryFilter.h" +#include "vtkCompositeDataSet.h" +#include "vtkGenericCell.h" +#include "vtkIdTypeArray.h" +#include "vtkMapper.h" +#include "vtkMatrix4x4.h" +#include "vtkObjectFactory.h" +#include "vtkPointData.h" +#include "vtkPoints.h" +#include "vtkPolyDataNormals.h" +#include "vtkProperty.h" +#include "vtkScalarsToColors.h" +#include "vtkSmartPointer.h" +#include "vtkTriangleFilter.h" +#include "vtkUnsignedCharArray.h" +#include "vtkWebGLDataSet.h" +#include "vtkWebGLExporter.h" +#include "vtkWebGLObject.h" + +#include +#include +#include +#include + +VTK_ABI_NAMESPACE_BEGIN +vtkStandardNewMacro(vtkWebGLPolyData); +//***************************************************************************** +class vtkWebGLPolyData::vtkInternal +{ +public: + std::vector Parts; + std::map IndexMap; +}; +//***************************************************************************** + +vtkWebGLPolyData::vtkWebGLPolyData() +{ + this->webGlType = wTRIANGLES; + this->iswidget = false; + this->Internal = new vtkInternal(); +} + +vtkWebGLPolyData::~vtkWebGLPolyData() +{ + vtkWebGLDataSet* obj; + while (!this->Internal->Parts.empty()) + { + obj = this->Internal->Parts.back(); + this->Internal->Parts.pop_back(); + obj->Delete(); + } + delete this->Internal; +} + +void vtkWebGLPolyData::SetMesh(float* _vertices, int _numberOfVertices, int* _index, + int _numberOfIndexes, float* _normals, unsigned char* _colors, float* _tcoords, int maxSize) +{ + this->webGlType = wTRIANGLES; + + vtkWebGLDataSet* obj; + while (!this->Internal->Parts.empty()) + { + obj = this->Internal->Parts.back(); + this->Internal->Parts.pop_back(); + obj->Delete(); + } + + short* index; + int div = maxSize * 3; + if (_numberOfVertices < div) + { + index = new short[_numberOfIndexes]; + for (int i = 0; i < _numberOfIndexes; i++) + index[i] = (short)_index[i]; + + obj = vtkWebGLDataSet::New(); + obj->SetVertices(_vertices, _numberOfVertices); + obj->SetIndexes(index, _numberOfIndexes); + obj->SetNormals(_normals); + obj->SetColors(_colors); + obj->SetMatrix(this->Matrix); + this->Internal->Parts.push_back(obj); + } + else + { + int total = _numberOfIndexes; + int curr = 0; + int size = 0; + + while (curr < total) + { + if (div + curr > total) + size = total - curr; + else + size = div; + + float* vertices = new float[size * 3]; + float* normals = new float[size * 3]; + unsigned char* colors = new unsigned char[size * 4]; + short* indexes = new short[size]; + float* tcoord = nullptr; + if (_tcoords) + tcoord = new float[size * 2]; + + this->Internal->IndexMap.clear(); + int count = 0; + for (int j = 0; j < size; j++) + { + int ind = _index[curr + j]; + if (this->Internal->IndexMap.find(ind) == this->Internal->IndexMap.end()) + { + vertices[count * 3 + 0] = _vertices[ind * 3 + 0]; + vertices[count * 3 + 1] = _vertices[ind * 3 + 1]; + vertices[count * 3 + 2] = _vertices[ind * 3 + 2]; + + normals[count * 3 + 0] = _normals[ind * 3 + 0]; + normals[count * 3 + 1] = _normals[ind * 3 + 1]; + normals[count * 3 + 2] = _normals[ind * 3 + 2]; + + colors[count * 4 + 0] = _colors[ind * 4 + 0]; + colors[count * 4 + 1] = _colors[ind * 4 + 1]; + colors[count * 4 + 2] = _colors[ind * 4 + 2]; + colors[count * 4 + 3] = _colors[ind * 4 + 3]; + + if (_tcoords) + { + tcoord[count * 2 + 0] = _tcoords[ind * 2 + 0]; + tcoord[count * 2 + 1] = _tcoords[ind * 2 + 1]; + } + this->Internal->IndexMap[ind] = count; + indexes[j] = count++; + } + else + { + indexes[j] = this->Internal->IndexMap[ind]; + } + } + curr += size; + float* v = new float[count * 3]; + memcpy(v, vertices, count * 3 * sizeof(float)); + delete[] vertices; + float* n = new float[count * 3]; + memcpy(n, normals, count * 3 * sizeof(float)); + delete[] normals; + unsigned char* c = new unsigned char[count * 4]; + memcpy(c, colors, count * 4); + delete[] colors; + obj = vtkWebGLDataSet::New(); + obj->SetVertices(v, count); + obj->SetIndexes(indexes, size); + obj->SetNormals(n); + obj->SetColors(c); + if (_tcoords) + { + float* tc = new float[count * 2]; + memcpy(tc, tcoord, count * 2 * sizeof(float)); + delete[] tcoord; + obj->SetTCoords(tc); + } + obj->SetMatrix(this->Matrix); + this->Internal->Parts.push_back(obj); + } + + delete[] _vertices; + delete[] _index; + delete[] _normals; + delete[] _colors; + delete[] _tcoords; + } +} + +void vtkWebGLPolyData::SetLine(float* _points, int _numberOfPoints, int* _index, int _numberOfIndex, + unsigned char* _colors, int maxSize) +{ + this->webGlType = wLINES; + + vtkWebGLDataSet* obj; + while (!this->Internal->Parts.empty()) + { + obj = this->Internal->Parts.back(); + this->Internal->Parts.pop_back(); + obj->Delete(); + } + + short* index; + int div = maxSize * 2; + if (_numberOfPoints < div) + { + index = new short[_numberOfIndex]; + for (int i = 0; i < _numberOfIndex; i++) + index[i] = (short)((unsigned int)_index[i]); + obj = vtkWebGLDataSet::New(); + obj->SetPoints(_points, _numberOfPoints); + obj->SetIndexes(index, _numberOfIndex); + obj->SetColors(_colors); + obj->SetMatrix(this->Matrix); + this->Internal->Parts.push_back(obj); + } + else + { + int total = _numberOfIndex; + int curr = 0; + int size = 0; + + while (curr < total) + { + if (div + curr > total) + size = total - curr; + else + size = div; + + float* points = new float[size * 3]; + unsigned char* colors = new unsigned char[size * 4]; + short* indexes = new short[size]; + + for (int j = 0; j < size; j++) + { + indexes[j] = j; + + points[j * 3 + 0] = _points[_index[curr + j] * 3 + 0]; + points[j * 3 + 1] = _points[_index[curr + j] * 3 + 1]; + points[j * 3 + 2] = _points[_index[curr + j] * 3 + 2]; + + colors[j * 4 + 0] = _colors[_index[curr + j] * 4 + 0]; + colors[j * 4 + 1] = _colors[_index[curr + j] * 4 + 1]; + colors[j * 4 + 2] = _colors[_index[curr + j] * 4 + 2]; + colors[j * 4 + 3] = _colors[_index[curr + j] * 4 + 3]; + } + curr += size; + obj = vtkWebGLDataSet::New(); + obj->SetPoints(points, size); + obj->SetIndexes(indexes, size); + obj->SetColors(colors); + obj->SetMatrix(this->Matrix); + this->Internal->Parts.push_back(obj); + } + delete[] _points; + delete[] _index; + delete[] _colors; + } +} + +void vtkWebGLPolyData::SetTransformationMatrix(vtkMatrix4x4* m) +{ + this->Superclass::SetTransformationMatrix(m); + for (size_t i = 0; i < this->Internal->Parts.size(); i++) + { + this->Internal->Parts[i]->SetMatrix(this->Matrix); + } +} + +unsigned char* vtkWebGLPolyData::GetBinaryData(int part) +{ + this->hasChanged = false; + vtkWebGLDataSet* obj = this->Internal->Parts[part]; + return obj->GetBinaryData(); +} + +int vtkWebGLPolyData::GetBinarySize(int part) +{ + vtkWebGLDataSet* obj = this->Internal->Parts[part]; + return obj->GetBinarySize(); +} + +void vtkWebGLPolyData::GenerateBinaryData() +{ + vtkWebGLDataSet* obj; + this->hasChanged = false; + std::stringstream ss; + for (size_t i = 0; i < this->Internal->Parts.size(); i++) + { + obj = this->Internal->Parts[i]; + obj->GenerateBinaryData(); + ss << obj->GetMD5(); + } + if (!this->Internal->Parts.empty()) + { + std::string localMD5; + vtkWebGLExporter::ComputeMD5( + (const unsigned char*)ss.str().c_str(), static_cast(ss.str().size()), localMD5); + this->hasChanged = this->MD5 != localMD5; + this->MD5 = localMD5; + } + else + cout << "Warning: GenerateBinaryData() @ vtkWebGLObject: This isn\'t supposed to happen."; +} + +int vtkWebGLPolyData::GetNumberOfParts() +{ + return static_cast(this->Internal->Parts.size()); +} + +void vtkWebGLPolyData::PrintSelf(ostream& os, vtkIndent indent) +{ + this->Superclass::PrintSelf(os, indent); +} + +void vtkWebGLPolyData::GetLinesFromPolygon( + vtkMapper* mapper, vtkActor* actor, int lineMaxSize, double* edgeColor) +{ + vtkWebGLPolyData* object = this; + vtkDataSet* dataset = nullptr; + vtkSmartPointer tempDS; + vtkDataObject* dObj = mapper->GetInputDataObject(0, 0); + vtkCompositeDataSet* cd = vtkCompositeDataSet::SafeDownCast(dObj); + if (cd) + { + vtkCompositeDataGeometryFilter* gf = vtkCompositeDataGeometryFilter::New(); + gf->SetInputData(cd); + gf->Update(); + tempDS = gf->GetOutput(); + gf->Delete(); + dataset = tempDS; + } + else + { + dataset = mapper->GetInput(); + } + + int np = 0; + int size = 0; + for (int i = 0; i < dataset->GetNumberOfCells(); i++) + size += dataset->GetCell(i)->GetNumberOfPoints(); + + float* points = new float[size * 3]; + unsigned char* color = new unsigned char[size * 4]; + int* index = new int[size * 2]; + double* point; + int pos = 0; + + vtkScalarsToColors* table = mapper->GetLookupTable(); + vtkDataArray* array; + if (mapper->GetScalarMode() == VTK_SCALAR_MODE_USE_CELL_FIELD_DATA) + { + vtkCellData* celldata = dataset->GetCellData(); + if (actor->GetMapper()->GetArrayAccessMode() == VTK_GET_ARRAY_BY_ID) + array = celldata->GetArray(actor->GetMapper()->GetArrayId()); + else + array = celldata->GetArray(actor->GetMapper()->GetArrayName()); + } + else + { + vtkPointData* pointdata = dataset->GetPointData(); + if (actor->GetMapper()->GetArrayAccessMode() == VTK_GET_ARRAY_BY_ID) + array = pointdata->GetArray(actor->GetMapper()->GetArrayId()); + else + array = pointdata->GetArray(actor->GetMapper()->GetArrayName()); + } + + int colorComponent = table->GetVectorComponent(); + int numberOfComponents = 0; + if (array != nullptr) + numberOfComponents = array->GetNumberOfComponents(); + int mode = table->GetVectorMode(); + double mag = 0, rgb[3]; + int curr = 0; + for (int i = 0; i < dataset->GetNumberOfCells(); i++) + { + vtkCell* cell = dataset->GetCell(i); + int b = pos; + np = dataset->GetCell(i)->GetNumberOfPoints(); + for (int j = 0; j < np; j++) + { + point = cell->GetPoints()->GetPoint(j); + points[curr * 3 + j * 3 + 0] = point[0]; + points[curr * 3 + j * 3 + 1] = point[1]; + points[curr * 3 + j * 3 + 2] = point[2]; + + index[curr * 2 + j * 2 + 0] = pos++; + index[curr * 2 + j * 2 + 1] = pos; + if (j == np - 1) + index[curr * 2 + j * 2 + 1] = b; + + vtkIdType pointId = cell->GetPointIds()->GetId(j); + if (numberOfComponents == 0) + { + actor->GetProperty()->GetColor(rgb); + } + else + { + switch (mode) + { + case vtkScalarsToColors::MAGNITUDE: + mag = 0; + for (int w = 0; w < numberOfComponents; w++) + mag += array->GetComponent(pointId, w) * array->GetComponent(pointId, w); + mag = sqrt(mag); + table->GetColor(mag, &rgb[0]); + break; + case vtkScalarsToColors::COMPONENT: + mag = array->GetComponent(pointId, colorComponent); + table->GetColor(mag, &rgb[0]); + break; + case vtkScalarsToColors::RGBCOLORS: + array->GetTuple(pointId, &rgb[0]); + break; + } + } + if (edgeColor != nullptr) + memcpy(rgb, edgeColor, sizeof(double) * 3); + color[curr * 4 + j * 4 + 0] = (unsigned char)((int)(rgb[0] * 255)); + color[curr * 4 + j * 4 + 1] = (unsigned char)((int)(rgb[1] * 255)); + color[curr * 4 + j * 4 + 2] = (unsigned char)((int)(rgb[2] * 255)); + color[curr * 4 + j * 4 + 3] = (unsigned char)255; + } + curr += np; + } + object->SetLine(points, size, index, size * 2, color, lineMaxSize); +} + +void vtkWebGLPolyData::GetLines(vtkTriangleFilter* polydata, vtkActor* actor, int lineMaxSize) +{ + vtkWebGLPolyData* object = this; + vtkCellArray* lines = polydata->GetOutput(0)->GetLines(); + + // Index + // Array of 3 Values. [#number of index, i1, i2] + // Discarting the first value + vtkDataArray* conn = lines->GetConnectivityArray(); + const vtkIdType connSize = conn->GetNumberOfValues(); + int* index = new int[static_cast(connSize)]; + for (vtkIdType i = 0; i < connSize; ++i) + { + index[i] = static_cast(conn->GetComponent(i, 0)); + } + // Point + double point[3]; + float* points = new float[polydata->GetOutput(0)->GetNumberOfPoints() * 3]; + for (int i = 0; i < polydata->GetOutput(0)->GetNumberOfPoints(); i++) + { + polydata->GetOutput(0)->GetPoint(i, point); + points[i * 3 + 0] = point[0]; + points[i * 3 + 1] = point[1]; + points[i * 3 + 2] = point[2]; + } + // Colors + unsigned char* color = new unsigned char[polydata->GetOutput(0)->GetNumberOfPoints() * 4]; + this->GetColorsFromPolyData(color, polydata->GetOutput(0), actor); + + object->SetLine(points, polydata->GetOutput(0)->GetNumberOfPoints(), index, + static_cast(connSize), color, lineMaxSize); +} + +void vtkWebGLPolyData::SetPoints( + float* points, int numberOfPoints, unsigned char* colors, int maxSize) +{ + this->webGlType = wPOINTS; + + // Delete Old Objects + vtkWebGLDataSet* obj; + while (!this->Internal->Parts.empty()) + { + obj = this->Internal->Parts.back(); + this->Internal->Parts.pop_back(); + obj->Delete(); + } + + // Create new objs + int numObjs = (numberOfPoints / maxSize) + 1; + int offset = 0; + int size = 0; + for (int i = 0; i < numObjs; i++) + { + size = numberOfPoints - offset; + if (size > maxSize) + size = maxSize; + + float* _points = new float[size * 3]; + unsigned char* _colors = new unsigned char[size * 4]; + memcpy(_points, &points[offset * 3], size * 3 * sizeof(float)); + memcpy(_colors, &colors[offset * 4], size * 4 * sizeof(unsigned char)); + + obj = vtkWebGLDataSet::New(); + obj->SetPoints(_points, size); + obj->SetColors(_colors); + obj->SetType(wPOINTS); + obj->SetMatrix(this->Matrix); + this->Internal->Parts.push_back(obj); + + offset += size; + } + + delete[] points; + delete[] colors; +} + +void vtkWebGLPolyData::GetPoints(vtkTriangleFilter* polydata, vtkActor* actor, int maxSize) +{ + vtkWebGLPolyData* object = this; + + // Points + double point[3]; + float* points = new float[polydata->GetOutput(0)->GetNumberOfPoints() * 3]; + for (int i = 0; i < polydata->GetOutput(0)->GetNumberOfPoints(); i++) + { + polydata->GetOutput(0)->GetPoint(i, point); + points[i * 3 + 0] = point[0]; + points[i * 3 + 1] = point[1]; + points[i * 3 + 2] = point[2]; + } + // Colors + unsigned char* colors = new unsigned char[polydata->GetOutput(0)->GetNumberOfPoints() * 4]; + this->GetColorsFromPolyData(colors, polydata->GetOutput(0), actor); + + object->SetPoints(points, polydata->GetOutput(0)->GetNumberOfPoints(), colors, maxSize); +} + +void vtkWebGLPolyData::GetColorsFromPolyData( + unsigned char* color, vtkPolyData* polydata, vtkActor* actor) +{ + int celldata; + vtkDataArray* array = vtkAbstractMapper::GetScalars(polydata, actor->GetMapper()->GetScalarMode(), + actor->GetMapper()->GetArrayAccessMode(), actor->GetMapper()->GetArrayId(), + actor->GetMapper()->GetArrayName(), celldata); + if (actor->GetMapper()->GetScalarVisibility() && array != nullptr) + { + vtkScalarsToColors* table = actor->GetMapper()->GetLookupTable(); + + vtkUnsignedCharArray* cor = + table->MapScalars(array, table->GetVectorMode(), table->GetVectorComponent()); + memcpy(color, cor->GetPointer(0), polydata->GetNumberOfPoints() * 4); + cor->Delete(); + } + else + { + for (int i = 0; i < polydata->GetNumberOfPoints(); i++) + { + color[i * 4 + 0] = (unsigned char)255; + color[i * 4 + 1] = (unsigned char)255; + color[i * 4 + 2] = (unsigned char)255; + color[i * 4 + 3] = (unsigned char)255; + } + } +} + +void vtkWebGLPolyData::GetPolygonsFromPointData( + vtkTriangleFilter* polydata, vtkActor* actor, int maxSize) +{ + vtkWebGLPolyData* object = this; + + vtkPolyDataNormals* polynormals = vtkPolyDataNormals::New(); + polynormals->SetInputConnection(polydata->GetOutputPort(0)); + polynormals->Update(); + + vtkPolyData* data = polynormals->GetOutput(); + + vtkCellArray* poly = data->GetPolys(); + vtkPointData* point = data->GetPointData(); + vtkNew ndata; + poly->ExportLegacyFormat(ndata); + vtkDataSetAttributes* attr = (vtkDataSetAttributes*)point; + + // Vertices + float* vertices = new float[data->GetNumberOfPoints() * 3]; + for (int i = 0; i < data->GetNumberOfPoints() * 3; i++) + vertices[i] = data->GetPoint(i / 3)[i % 3]; + // Index + // ndata contain 4 values for the normal: [number of values per index, index[3]] + // We don't need the first value + int* indexes = new int[ndata->GetSize() * 3 / 4]; + for (int i = 0; i < ndata->GetSize(); i++) + if (i % 4 != 0) + indexes[i * 3 / 4] = ndata->GetValue(i); + // Normal + float* normal = new float[attr->GetNormals()->GetSize()]; + for (int i = 0; i < attr->GetNormals()->GetSize(); i++) + normal[i] = attr->GetNormals()->GetComponent(0, i); + // Colors + unsigned char* color = new unsigned char[data->GetNumberOfPoints() * 4]; + this->GetColorsFromPointData(color, point, data, actor); + // TCoord + float* tcoord = nullptr; + if (attr->GetTCoords()) + { + tcoord = new float[attr->GetTCoords()->GetSize()]; + for (int i = 0; i < attr->GetTCoords()->GetSize(); i++) + tcoord[i] = attr->GetTCoords()->GetComponent(0, i); + } + + object->SetMesh(vertices, data->GetNumberOfPoints(), indexes, ndata->GetSize() * 3 / 4, normal, + color, tcoord, maxSize); + polynormals->Delete(); +} + +void vtkWebGLPolyData::GetPolygonsFromCellData( + vtkTriangleFilter* polydata, vtkActor* actor, int maxSize) +{ + vtkWebGLPolyData* object = this; + + vtkPolyDataNormals* polynormals = vtkPolyDataNormals::New(); + polynormals->SetInputConnection(polydata->GetOutputPort(0)); + polynormals->Update(); + + vtkPolyData* data = polynormals->GetOutput(); + vtkCellData* celldata = data->GetCellData(); + + vtkDataArray* array; + if (actor->GetMapper()->GetArrayAccessMode() == VTK_GET_ARRAY_BY_ID) + array = celldata->GetArray(actor->GetMapper()->GetArrayId()); + else + array = celldata->GetArray(actor->GetMapper()->GetArrayName()); + vtkScalarsToColors* table = actor->GetMapper()->GetLookupTable(); + int colorComponent = table->GetVectorComponent(); + int mode = table->GetVectorMode(); + + float* vertices = new float[data->GetNumberOfCells() * 3 * 3]; + float* normals = new float[data->GetNumberOfCells() * 3 * 3]; + unsigned char* colors = new unsigned char[data->GetNumberOfCells() * 3 * 4]; + int* indexes = new int[data->GetNumberOfCells() * 3 * 3]; + + vtkGenericCell* cell = vtkGenericCell::New(); + double tuple[3], normal[3], color[3]; + color[0] = 1.0; + color[1] = 1.0; + color[2] = 1.0; + vtkPoints* points; + int aux; + double mag, alpha = 1.0; + int numberOfComponents = 0; + if (array) + numberOfComponents = array->GetNumberOfComponents(); + else + mode = -1; + for (int i = 0; i < data->GetNumberOfCells(); i++) + { + data->GetCell(i, cell); + points = cell->GetPoints(); + + // getColors + alpha = 1.0; + switch (mode) + { + case -1: + actor->GetProperty()->GetColor(color); + alpha = actor->GetProperty()->GetOpacity(); + break; + case vtkScalarsToColors::MAGNITUDE: + mag = 0; + for (int w = 0; w < numberOfComponents; w++) + mag += array->GetComponent(i, w) * array->GetComponent(i, w); + mag = sqrt(mag); + table->GetColor(mag, &color[0]); + alpha = table->GetOpacity(mag); + break; + case vtkScalarsToColors::COMPONENT: + mag = array->GetComponent(i, colorComponent); + table->GetColor(mag, &color[0]); + alpha = table->GetOpacity(mag); + break; + case vtkScalarsToColors::RGBCOLORS: + array->GetTuple(i, &color[0]); + break; + } + // getNormals + celldata->GetNormals()->GetTuple(i, &normal[0]); + for (int j = 0; j < 3; j++) + { + aux = i * 9 + j * 3; + // Normals + normals[aux + 0] = normal[0]; + normals[aux + 1] = normal[1]; + normals[aux + 2] = normal[2]; + // getVertices + points->GetPoint(j, &tuple[0]); + vertices[aux + 0] = tuple[0]; + vertices[aux + 1] = tuple[1]; + vertices[aux + 2] = tuple[2]; + // Colors + colors[4 * (3 * i + j) + 0] = (unsigned char)((int)(color[0] * 255)); + colors[4 * (3 * i + j) + 1] = (unsigned char)((int)(color[1] * 255)); + colors[4 * (3 * i + j) + 2] = (unsigned char)((int)(color[2] * 255)); + colors[4 * (3 * i + j) + 3] = (unsigned char)((int)(alpha * 255)); + // getIndexes + indexes[aux + 0] = aux + 0; + indexes[aux + 1] = aux + 1; + indexes[aux + 2] = aux + 2; + } + } + object->SetMesh(vertices, data->GetNumberOfCells() * 3, indexes, data->GetNumberOfCells() * 3, + normals, colors, nullptr, maxSize); + cell->Delete(); + polynormals->Delete(); +} + +void vtkWebGLPolyData::GetColorsFromPointData( + unsigned char* color, vtkPointData* pointdata, vtkPolyData* polydata, vtkActor* actor) +{ + vtkDataSetAttributes* attr = (vtkDataSetAttributes*)pointdata; + + int colorSize = attr->GetNormals()->GetSize() * 4 / 3; + + vtkDataArray* array; + if (actor->GetMapper()->GetArrayAccessMode() == VTK_GET_ARRAY_BY_ID) + array = pointdata->GetArray(actor->GetMapper()->GetArrayId()); + else + array = pointdata->GetArray(actor->GetMapper()->GetArrayName()); + + if (array && actor->GetMapper()->GetScalarVisibility() && + actor->GetMapper()->GetArrayName() != nullptr && actor->GetMapper()->GetArrayName()[0] != '\0') + { + vtkScalarsToColors* table = actor->GetMapper()->GetLookupTable(); + int colorComponent = table->GetVectorComponent(), + numberOfComponents = array->GetNumberOfComponents(); + int mode = table->GetVectorMode(); + double mag = 0, rgb[3]; + double alpha = 1.0; + + if (numberOfComponents == 1 && mode == vtkScalarsToColors::MAGNITUDE) + { + mode = vtkScalarsToColors::COMPONENT; + colorComponent = 0; + } + for (int i = 0; i < colorSize / 4; i++) + { + switch (mode) + { + case vtkScalarsToColors::MAGNITUDE: + mag = 0; + for (int w = 0; w < numberOfComponents; w++) + mag += array->GetComponent(i, w) * array->GetComponent(i, w); + mag = sqrt(mag); + table->GetColor(mag, &rgb[0]); + alpha = table->GetOpacity(mag); + break; + case vtkScalarsToColors::COMPONENT: + mag = array->GetComponent(i, colorComponent); + table->GetColor(mag, &rgb[0]); + alpha = table->GetOpacity(mag); + break; + case vtkScalarsToColors::RGBCOLORS: + array->GetTuple(i, &rgb[0]); + alpha = actor->GetProperty()->GetOpacity(); + break; + } + color[i * 4 + 0] = (unsigned char)((int)(rgb[0] * 255)); + color[i * 4 + 1] = (unsigned char)((int)(rgb[1] * 255)); + color[i * 4 + 2] = (unsigned char)((int)(rgb[2] * 255)); + color[i * 4 + 3] = (unsigned char)((int)(alpha * 255)); + } + } + else + { + double rgb[3]; + double alpha = 0; + int celldata; + array = vtkAbstractMapper::GetScalars(polydata, actor->GetMapper()->GetScalarMode(), + actor->GetMapper()->GetArrayAccessMode(), actor->GetMapper()->GetArrayId(), + actor->GetMapper()->GetArrayName(), celldata); + if (actor->GetMapper()->GetScalarVisibility() && + (actor->GetMapper()->GetColorMode() == VTK_COLOR_MODE_DEFAULT || + actor->GetMapper()->GetColorMode() == VTK_COLOR_MODE_DIRECT_SCALARS) && + array) + { + vtkScalarsToColors* table = actor->GetMapper()->GetLookupTable(); + vtkUnsignedCharArray* cor = + table->MapScalars(array, actor->GetMapper()->GetColorMode(), table->GetVectorComponent()); + memcpy(color, cor->GetPointer(0), polydata->GetNumberOfPoints() * 4); + cor->Delete(); + } + else + { + actor->GetProperty()->GetColor(rgb); + alpha = actor->GetProperty()->GetOpacity(); + for (int i = 0; i < colorSize / 4; i++) + { + color[i * 4 + 0] = (unsigned char)((int)(rgb[0] * 255)); + color[i * 4 + 1] = (unsigned char)((int)(rgb[1] * 255)); + color[i * 4 + 2] = (unsigned char)((int)(rgb[2] * 255)); + color[i * 4 + 3] = (unsigned char)((int)(alpha * 255)); + } + } + } +} +VTK_ABI_NAMESPACE_END diff --git a/Web/WebGLExporter/vtkWebGLPolyData.h b/Web/WebGLExporter/vtkWebGLPolyData.h new file mode 100644 index 000000000..12e5a3e77 --- /dev/null +++ b/Web/WebGLExporter/vtkWebGLPolyData.h @@ -0,0 +1,66 @@ +// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen +// SPDX-License-Identifier: BSD-3-Clause +/** + * @class vtkWebGLPolyData + * @brief PolyData representation for WebGL. + */ + +#ifndef vtkWebGLPolyData_h +#define vtkWebGLPolyData_h + +#include "vtkWebGLExporterModule.h" // needed for export macro +#include "vtkWebGLObject.h" + +VTK_ABI_NAMESPACE_BEGIN +class vtkActor; +class vtkMatrix4x4; +class vtkMapper; +class vtkPointData; +class vtkPolyData; +class vtkTriangleFilter; + +class VTKWEBGLEXPORTER_EXPORT vtkWebGLPolyData : public vtkWebGLObject +{ +public: + static vtkWebGLPolyData* New(); + vtkTypeMacro(vtkWebGLPolyData, vtkWebGLObject); + void PrintSelf(ostream& os, vtkIndent indent) override; + + void GenerateBinaryData() override; + unsigned char* GetBinaryData(int part) override; + int GetBinarySize(int part) override; + int GetNumberOfParts() override; + + void GetPoints(vtkTriangleFilter* polydata, vtkActor* actor, int maxSize); + + void GetLinesFromPolygon(vtkMapper* mapper, vtkActor* actor, int lineMaxSize, double* edgeColor); + void GetLines(vtkTriangleFilter* polydata, vtkActor* actor, int lineMaxSize); + void GetColorsFromPolyData(unsigned char* color, vtkPolyData* polydata, vtkActor* actor); + + // Get following data from the actor + void GetPolygonsFromPointData(vtkTriangleFilter* polydata, vtkActor* actor, int maxSize); + void GetPolygonsFromCellData(vtkTriangleFilter* polydata, vtkActor* actor, int maxSize); + void GetColorsFromPointData( + unsigned char* color, vtkPointData* pointdata, vtkPolyData* polydata, vtkActor* actor); + + void SetMesh(float* _vertices, int _numberOfVertices, int* _index, int _numberOfIndexes, + float* _normals, unsigned char* _colors, float* _tcoords, int maxSize); + void SetLine(float* _points, int _numberOfPoints, int* _index, int _numberOfIndex, + unsigned char* _colors, int maxSize); + void SetPoints(float* points, int numberOfPoints, unsigned char* colors, int maxSize); + void SetTransformationMatrix(vtkMatrix4x4* m); + +protected: + vtkWebGLPolyData(); + ~vtkWebGLPolyData() override; + +private: + vtkWebGLPolyData(const vtkWebGLPolyData&) = delete; + void operator=(const vtkWebGLPolyData&) = delete; + + class vtkInternal; + vtkInternal* Internal; +}; + +VTK_ABI_NAMESPACE_END +#endif diff --git a/Web/WebGLExporter/vtkWebGLWidget.cxx b/Web/WebGLExporter/vtkWebGLWidget.cxx new file mode 100644 index 000000000..ec2f21cef --- /dev/null +++ b/Web/WebGLExporter/vtkWebGLWidget.cxx @@ -0,0 +1,157 @@ +// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen +// SPDX-License-Identifier: BSD-3-Clause + +#include "vtkWebGLWidget.h" + +#include "vtkActor2D.h" +#include "vtkDiscretizableColorTransferFunction.h" +#include "vtkObjectFactory.h" +#include "vtkScalarBarActor.h" +#include "vtkWebGLExporter.h" +#include "vtkWebGLObject.h" + +#include + +VTK_ABI_NAMESPACE_BEGIN +vtkStandardNewMacro(vtkWebGLWidget); + +vtkWebGLWidget::vtkWebGLWidget() +{ + this->binaryData = nullptr; + this->iswidget = false; + this->binarySize = 0; + this->orientation = 1; + this->interactAtServer = false; + this->title = nullptr; +} + +vtkWebGLWidget::~vtkWebGLWidget() +{ + delete[] this->binaryData; + while (!this->colors.empty()) + { + double* xrgb = this->colors.back(); + this->colors.pop_back(); + delete[] xrgb; + } + delete[] this->title; +} + +unsigned char* vtkWebGLWidget::GetBinaryData(int vtkNotUsed(part)) +{ + this->hasChanged = false; + return this->binaryData; +} + +int vtkWebGLWidget::GetBinarySize(int vtkNotUsed(part)) +{ + return this->binarySize; +} + +void vtkWebGLWidget::GenerateBinaryData() +{ + delete[] this->binaryData; + std::string oldMD5 = this->MD5; + + size_t pos = 0; + // Calculate the size used + // NumOfColors, Type, Position, Size, Colors, Orientation, numberOfLabels + int total = static_cast(sizeof(int) + 1 + 4 * sizeof(float) + + this->colors.size() * (sizeof(float) + 3 * sizeof(char)) + 1 + 1 + strlen(this->title)); + this->binaryData = new unsigned char[total]; + int colorSize = static_cast(this->colors.size()); + + memset(this->binaryData, 0, total); // Fill array with 0 + memcpy(&this->binaryData[pos], &colorSize, sizeof(int)); + pos += sizeof(int); // Binary Data Size + this->binaryData[pos++] = 'C'; // Object Type + memcpy(&this->binaryData[pos], &this->position, sizeof(float) * 2); + pos += sizeof(float) * 2; // Position (double[2]) + memcpy(&this->binaryData[pos], &this->size, sizeof(float) * 2); + pos += sizeof(float) * 2; // Size (double[2]) + unsigned char rgb[3]; + for (size_t i = 0; i < colors.size(); i++) // Array of Colors (double, char[3]) + { + float v = (float)this->colors[i][0]; + memcpy(&this->binaryData[pos], &v, sizeof(float)); + pos += sizeof(float); + rgb[0] = (unsigned char)((int)(this->colors[i][1] * 255)); + rgb[1] = (unsigned char)((int)(this->colors[i][2] * 255)); + rgb[2] = (unsigned char)((int)(this->colors[i][3] * 255)); + memcpy(&this->binaryData[pos], rgb, 3 * sizeof(unsigned char)); + pos += sizeof(unsigned char) * 3; + } + unsigned char aux; + aux = (unsigned char)this->orientation; + memcpy(&this->binaryData[pos], &aux, 1); + pos++; + aux = (unsigned char)this->numberOfLabels; + memcpy(&this->binaryData[pos], &aux, 1); + pos++; + memcpy(&this->binaryData[pos], this->title, strlen(this->title)); + pos += strlen(this->title); + + this->binarySize = total; + vtkWebGLExporter::ComputeMD5(this->binaryData, total, this->MD5); + this->hasChanged = this->MD5 != oldMD5; +} + +int vtkWebGLWidget::GetNumberOfParts() +{ + return 1; +} + +void vtkWebGLWidget::PrintSelf(ostream& os, vtkIndent indent) +{ + this->Superclass::PrintSelf(os, indent); +} + +void vtkWebGLWidget::GetDataFromColorMap(vtkActor2D* actor) +{ + vtkScalarBarActor* scalarbar = vtkScalarBarActor::SafeDownCast(actor); + this->numberOfLabels = scalarbar->GetNumberOfLabels(); + + std::stringstream theTitle; + char* componentTitle = scalarbar->GetComponentTitle(); + + theTitle << scalarbar->GetTitle(); + if (componentTitle && strlen(componentTitle) > 0) + { + theTitle << " "; + theTitle << componentTitle; + } + + delete[] this->title; + std::string tmp = theTitle.str(); + this->title = new char[tmp.length() + 1]; + strcpy(this->title, tmp.c_str()); + this->hasTransparency = (scalarbar->GetUseOpacity() != 0); + this->orientation = scalarbar->GetOrientation(); + + // Colors + vtkDiscretizableColorTransferFunction* lookup = + vtkDiscretizableColorTransferFunction::SafeDownCast(scalarbar->GetLookupTable()); + int num = 5 * lookup->GetSize(); + double* range = lookup->GetRange(); + double v, s; + v = range[0]; + s = (range[1] - range[0]) / (num - 1); + for (int i = 0; i < num; i++) + { + double* xrgb = new double[4]; + scalarbar->GetLookupTable()->GetColor(v, &xrgb[1]); + xrgb[0] = v; + this->colors.push_back(xrgb); + v += s; + } + + this->textFormat = scalarbar->GetLabelFormat(); // Float Format ex.: %-#6.3g + this->textPosition = scalarbar->GetTextPosition(); // Orientacao dos textos; 1; + double* thePos = scalarbar->GetPosition(); + double* theSize = scalarbar->GetPosition2(); + this->position[0] = thePos[0]; + this->position[1] = thePos[1]; // Widget Position + this->size[0] = theSize[0]; + this->size[1] = theSize[1]; // Widget Size +} +VTK_ABI_NAMESPACE_END diff --git a/Web/WebGLExporter/vtkWebGLWidget.h b/Web/WebGLExporter/vtkWebGLWidget.h new file mode 100644 index 000000000..90b9c9c29 --- /dev/null +++ b/Web/WebGLExporter/vtkWebGLWidget.h @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen +// SPDX-License-Identifier: BSD-3-Clause +/** + * @class vtkWebGLWidget + * @brief Widget representation for WebGL. + */ + +#ifndef vtkWebGLWidget_h +#define vtkWebGLWidget_h + +#include "vtkWebGLExporterModule.h" // needed for export macro +#include "vtkWebGLObject.h" + +#include // Needed to store colors + +VTK_ABI_NAMESPACE_BEGIN +class vtkActor2D; + +class VTKWEBGLEXPORTER_EXPORT vtkWebGLWidget : public vtkWebGLObject +{ +public: + static vtkWebGLWidget* New(); + vtkTypeMacro(vtkWebGLWidget, vtkWebGLObject); + void PrintSelf(ostream& os, vtkIndent indent) override; + + void GenerateBinaryData() override; + unsigned char* GetBinaryData(int part) override; + int GetBinarySize(int part) override; + int GetNumberOfParts() override; + + void GetDataFromColorMap(vtkActor2D* actor); + +protected: + vtkWebGLWidget(); + ~vtkWebGLWidget() override; + + unsigned char* binaryData; + int binarySize; + int orientation; + char* title; + char* textFormat; + int textPosition; + float position[2]; + float size[2]; + int numberOfLabels; + std::vector colors; // x, r, g, b + +private: + vtkWebGLWidget(const vtkWebGLWidget&) = delete; + void operator=(const vtkWebGLWidget&) = delete; +}; + +VTK_ABI_NAMESPACE_END +#endif diff --git a/Web/WebGLExporter/webglRenderer.js b/Web/WebGLExporter/webglRenderer.js new file mode 100644 index 000000000..e0e16445b --- /dev/null +++ b/Web/WebGLExporter/webglRenderer.js @@ -0,0 +1,1307 @@ +/** + * Create a renderer object working fully in WebGL + * Here is a sample set of command to illustrate how to use this renderer + * + * var renderer = new WebGLRenderer('rendererId','http://localhost:8080/ParaViewWebService') + * renderer.init(sessionId, viewId); + * renderer.bindToElementId('containerID'); // => Add a WebGL canvas inside a div tag id 'containerID' + * renderer.start(); + * + * renderer.init(otherSessionId, otherViewId); + * renderer.view.width = '100'; + * renderer.view.height = '400'; + * renderer.setSize('100', '400'); + * + * renderer.unbindToElementId('containerID'); + */ + +// Global object to keep track of WebGL renderers +var webglRenderers = new Object(); + +window.requestAnimFrame = (function(){ + return window.requestAnimationFrame || + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + window.oRequestAnimationFrame || + window.msRequestAnimationFrame || + function(/* function */ callback, /* DOMElement */ element){ + window.setTimeout(callback, 1000 / 60); + }; +})(); + +function WebGLRenderer(rendererId, coreServiceURL) { + this.baseURL = coreServiceURL + "/WebGL"; + this.rendererId = rendererId; + this.sessionId = ""; + this.viewId = ""; + this.nbError = 0; + this.localTimeStamp = 0; + this.offlineMode = false; + this.setServerMode(false); + this.forceSquareSize = false; + + this.view = new Object(); + this.view.width = 100; + this.view.height = 100; + this.view.id = rendererId; + this.view.alt = "ParaView Renderer"; + + //Default Shaders + this.view.shaderfs = document.createElement("script"); + this.view.shaderfs.id = "shader-fs"; + this.view.shaderfs.type = "x-shader/x-fragment"; + this.view.shaderfs.innerHTML = "\ + #ifdef GL_ES\n\ + precision highp float;\n\ + #endif\n\ + uniform bool uIsLine;\ + varying vec4 vColor;\ + varying vec4 vTransformedNormal;\ + varying vec4 vPosition;\ + void main(void) {\ + float directionalLightWeighting1 = max(dot(normalize(vTransformedNormal.xyz), vec3(0.0, 0.0, 1.0)), 0.0); \ + float directionalLightWeighting2 = max(dot(normalize(vTransformedNormal.xyz), vec3(0.0, 0.0, -1.0)), 0.0);\ + vec3 lightWeighting = max(vec3(1.0, 1.0, 1.0) * directionalLightWeighting1, vec3(1.0, 1.0, 1.0) * directionalLightWeighting2);\ + if (uIsLine == false){\ + gl_FragColor = vec4(vColor.rgb * lightWeighting, vColor.a);\ + } else {\ + gl_FragColor = vColor*vec4(1.0, 1.0, 1.0, 1.0);\ + }\ + }"; + this.view.shadervs = document.createElement("script"); + this.view.shadervs.id = "shader-vs"; + this.view.shadervs.type = "x-shader/x-vertex"; + this.view.shadervs.innerHTML = "\ + attribute vec3 aVertexPosition;\ + attribute vec4 aVertexColor;\ + attribute vec3 aVertexNormal;\ + uniform mat4 uMVMatrix;\ + uniform mat4 uPMatrix;\ + uniform mat4 uNMatrix;\ + varying vec4 vColor;\ + varying vec4 vPosition;\ + varying vec4 vTransformedNormal;\ + void main(void) {\ + vPosition = uMVMatrix * vec4(aVertexPosition, 1.0);\ + gl_Position = uPMatrix * vPosition;\ + vTransformedNormal = uNMatrix * vec4(aVertexNormal, 1.0);\ + vColor = aVertexColor;\ + }"; + + // Point Shaders + this.view.shaderfsPoint = document.createElement("script"); + this.view.shaderfsPoint.id = "shader-fs-Point"; + this.view.shaderfsPoint.type = "x-shader/x-fragment"; + this.view.shaderfsPoint.innerHTML = "\ + #ifdef GL_ES\n\ + precision highp float;\n\ + #endif\n\ + varying vec4 vColor;\ + void main(void) {\ + gl_FragColor = vColor;\ + }"; + this.view.shadervsPoint = document.createElement("script"); + this.view.shadervsPoint.id = "shader-vs-Point"; + this.view.shadervsPoint.type = "x-shader/x-vertex"; + this.view.shadervsPoint.innerHTML = "\ + attribute vec3 aVertexPosition;\ + attribute vec4 aVertexColor;\ + uniform mat4 uMVMatrix;\ + uniform mat4 uPMatrix;\ + uniform mat4 uNMatrix;\ + uniform float uPointSize;\ + varying vec4 vColor;\ + void main(void) {\ + vec4 pos = uMVMatrix * vec4(aVertexPosition, 1.0);\ + gl_Position = uPMatrix * pos;\ + vColor = aVertexColor*vec4(1.0, 1.0, 1.0, 1.0);\ + gl_PointSize = uPointSize;\ + }"; + + // + this.canvasName = "glcanvas" + rendererId; + this.view.html = '
Your browser doesn\'t appear to support the HTML5 \ + <canvas> element.'; + + this.view.html += '
'; + this.fps = 0; + + // Register in global var + webglRenderers[rendererId] = this; +} + +WebGLRenderer.prototype.bindToElementId = function (elementId) { + this.oldInnerHTML = document.getElementById(elementId).innerHTML; + document.getElementById(elementId).innerHTML = this.view.html; + + document.getElementById(elementId).appendChild(this.view.shaderfs); + document.getElementById(elementId).appendChild(this.view.shadervs); + document.getElementById(elementId).appendChild(this.view.shaderfsPoint); + document.getElementById(elementId).appendChild(this.view.shadervsPoint); +} + +WebGLRenderer.prototype.unbindToElementId = function (elementId) { + document.getElementById(elementId).innerHTML = this.oldInnerHTML; + clearTimeout(this.drawInterval); + if (typeof(paraview) != "undefined") paraview.updateConfiguration(true, "JPEG", "NO"); +} + +WebGLRenderer.prototype.setOfflineMode = function (mode) { + this.offlineMode = mode; + this.requestMetaData(); +} + +WebGLRenderer.prototype.bindToElement = function (element) { + this.oldInnerHTML = element.innerHTML; + element.innerHTML = this.view.html; + + element.appendChild(this.view.shaderfs); + element.appendChild(this.view.shadervs); + element.appendChild(this.view.shaderfsPoint); + element.appendChild(this.view.shadervsPoint); +} + +WebGLRenderer.prototype.unbindToElement = function (element) { + element.innerHTML = this.oldInnerHTML; + clearTimeout(this.drawInterval); + if (typeof(paraview) != "undefined") paraview.updateConfiguration(true, "JPEG", "NO"); +} + +WebGLRenderer.prototype.init = function (sessionId, viewId) { + this.sessionId = sessionId; + this.viewId = viewId; +} + +WebGLRenderer.prototype.start = function(metadata, objects) { + if (typeof(renderers) == "undefined"){ + renderers = Object(); + renderers.current = this; + } + if (typeof(paraview) != "undefined") paraview.updateConfiguration(true, "JPEG", "WebGL"); + canvas = document.getElementById(this.canvasName); + canvas.width = this.view.width; + canvas.height = this.view.height; + + this.hasSceneChanged = true; //Scene Graph Has Changed + this.oldCamPos = null; //Last Known Camera Position + this.sceneJSON = null; //Current Scene Graph + this.up = []; + this.right = []; + this.z_dir = []; + this.objects = []; //List of objects + this.nbErrors = 0; //Number of Errors + this.background = null; //Background object: mesh, normals, colors, render + this.interactionRatio = 2; + this.requestInterval = 250; //Frequency it request new data from the server + this.requestOldInterval = 250; // + this.updateInterval = 100; //Frequency the server will be updated + this.fps = 0; + this.frames = 0; + this.lastTime = new Date().getTime(); + this.view.aspectRatio = 1; + this.lookAt = [0,0,0,0,1,0,0,0,1]; + this.offlineMode = !(typeof(metadata)=="undefined" || typeof(objects)=="undefined"); + + this.cachedObjects = []; //List of Cached Objects + this.isCaching = false; //Is Caching or Not + + this.processQueue = []; //List of process to be executed + + this.objScale = 1.0; //Scale applied locally in the scene + this.translation = [0.0, 0.0, 0.0]; //Translation + this.rotMatrix = mat4.create(); //Rotation Matrix + mat4.identity(this.rotMatrix); + this.rotMatrix2 = mat4.create(this.rotMatrix); + + this.mouseDown = false; + this.lastMouseX = 0; + this.lastMouseY = 0; + + this.mvMatrix = mat4.create(this.rotMatrix); + this.pMatrix = mat4.create(this.rotMatrix); + + // Initialize the GL context + this.gl = null; + try { + this.gl = canvas.getContext("experimental-webgl"); + this.gl.viewportWidth = this.view.width; + this.gl.viewportHeight = this.view.height; + } catch(e) {} + + if (this.gl) { + this.gl.clearColor(0.0, 0.0, 0.0, 1.0); + this.gl.clearDepth(1.0); + this.gl.enable(this.gl.DEPTH_TEST); + this.gl.depthFunc(this.gl.LEQUAL); + + this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA); + + this.initShaders(); + + this.ctx2d = document.getElementById(this.canvasName + "Widget").getContext('2d'); + // Set up to draw the scene periodically. + this.drawInterval = requestAnimFrame(new Function("webglRenderers['" + this.view.id + "'].drawScene();")); + + if (!this.offlineMode){ + this.requestMetaData(); + this.updateCamera(); + } else { + this.sceneJSON = JSON.parse(metadata); + + for(aw=0; aw height) height = width; + else width = height; + } + this.view.width = width; + this.view.height = height; + canvas = document.getElementById(this.canvasName); + canvasWidget = document.getElementById(this.canvasName + "Widget"); + if (canvas){ + canvas.width = this.view.width; + canvas.height = this.view.height; + canvasWidget.width = this.view.width; + canvasWidget.height = this.view.height; + if (typeof(this.gl) != "undefined" && this.gl != null){ + if (!this.offlineMode) updateRendererSize(this.sessionId, this.viewId, width, height); + this.gl.viewportWidth = this.view.width; + this.gl.viewportHeight = this.view.height; + } + left = 0; tt = 0; + if (this.forceSquareSize){ + left = Math.round((w-this.view.width)/2); + tt = Math.round((h-this.view.height)/2); + } + this.view.left = left; + this.view.top = top; + if(this.forceSquareSize == true){ + canvas.setAttribute("style", "position: absolute; overflow: hidden; left: " + left + "px; top: " + tt + "px; right: 0px; z-index:0;"); + canvasWidget.setAttribute("style", "position: absolute; overflow: hidden; left: " + left + "px; top: " + tt + "px; right: 0px; z-index:1;"); + } else { + canvas.setAttribute("style", "overflow: hidden; left: " + left + "px; top: " + tt + "px; right: 0px; z-index:0;"); + canvasWidget.setAttribute("style", "position: absolute; overflow: hidden; left: " + left + "px; top: " + tt + "px; right: 0px; z-index:1;"); + } + } +} + +WebGLRenderer.prototype.requestMetaData = function() { + if (this.mouseDown || renderers.current != this) return; + if (this.offlineMode) return; + + interval = this.requestInterval; + if (this.serverMode) interval = interval/2; + this.timer = setTimeout("webglRenderers[\'" + this.view.id + "\'].requestMetaData()", interval); + var request = new XMLHttpRequest(); + request.requester = this; + filename = this.baseURL + "?sid=" + this.sessionId + "&vid=" + this.viewId + "&q=meta"; + try { + request.open("GET", filename, false); + request.overrideMimeType('text/plain; charset=x-user-defined'); + request.onreadystatechange = function() { + if(this.requester.mouseDown) return; + if (request.status != 200) this.requester.nbErrors++ + else if (request.readyState == 4) { + aux = JSON.parse(request.responseText); + this.requester.hasSceneChanged = JSON.stringify(aux)!=JSON.stringify(this.requester.sceneJSON); + this.requester.sceneJSON = JSON.parse(request.responseText); + if (this.requester.hasSceneChanged) this.requester.updateScene(); + } + } + request.send(); + } catch (e) { + this.nbErrors++; + } +} + +WebGLRenderer.prototype.updateScene = function(){ + if (typeof(this.sceneJSON) == "undefined" || this.sceneJSON == null) return; + c1 = [0,0,0]; + c2 = [0,0,0]; + for(l=0; l= 50 && this.nbErrors < 5){ + this.frames = 0; + ko = new Date(); + currTime = ko.getTime(); + diff = currTime - this.lastTime; + this.lastTime = currTime; + this.fps = 50000/diff; + } + this.processObject(); + + this.gl.viewport(0, 0, this.gl.viewportWidth, this.gl.viewportHeight); + this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT); + + mat4.ortho(-1.0, 1.0, -1.0, 1.0, 1.0, 1000000.0, this.pMatrix); + mat4.identity(this.mvMatrix); + this.gl.disable(this.gl.DEPTH_TEST); + this.renderBackground(); + this.gl.enable(this.gl.DEPTH_TEST); + + this.ctx2d.clearRect(0, 0, this.view.width, this.view.height); + for(rr=this.sceneJSON.Renderers.length-1; rr>=0 ; rr--){ + renderer = this.sceneJSON.Renderers[rr]; + width = renderer.size[0]-renderer.origin[0]; + height = renderer.size[1]-renderer.origin[1]; + width = width*this.view.width; + height = height*this.view.height; + x = renderer.origin[0]*this.view.width; + y = renderer.origin[1]*this.view.height; + if (y < 0) y = 0; + this.gl.viewport(x, y, width, height); + //this.gl.clear(this.gl.DEPTH_BUFFER_BIT); + mat4.perspective(renderer.LookAt[0], width/height, 0.1, 1000000.0, this.pMatrix); + mat4.identity(this.mvMatrix); + mat4.lookAt([renderer.LookAt[7], renderer.LookAt[8], renderer.LookAt[9]], + [renderer.LookAt[1], renderer.LookAt[2], renderer.LookAt[3]], + [renderer.LookAt[4], renderer.LookAt[5], renderer.LookAt[6]], + this.mvMatrix); + + for(r=0; r